├── .github └── workflows │ └── main.yml ├── .gitignore ├── README.md ├── binding.gyp ├── index.d.ts ├── main.js ├── package-lock.json ├── package.json ├── spec ├── core.spec.ts ├── helper.ts └── plugins.spec.ts ├── src ├── bindings │ ├── b_accounts.c │ ├── b_accounts.h │ ├── b_buddy.c │ ├── b_buddy.h │ ├── b_core.c │ ├── b_core.h │ ├── b_notify.c │ ├── b_notify.h │ ├── b_plugins.c │ └── b_plugins.h ├── eventloop.c ├── eventloop.h ├── helper.c ├── helper.h ├── messaging.c ├── messaging.h ├── module.c ├── napi_helpers.c ├── napi_helpers.h ├── signalling.c └── signalling.h ├── test.js └── tsconfig.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node_version: ['20', '21'] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Install dependencies 20 | run: sudo apt update && sudo apt install --no-install-recommends -y libpurple-dev libglib2.0-dev python3 21 | - name: Use Node.js ${{ matrix.node_version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node_version }} 25 | - name: Install node dependencies and build library 26 | run: npm ci 27 | - name: Run tests 28 | # NASTY hack because we can't get libpurple to load. 29 | run: LD_PRELOAD=/lib/x86_64-linux-gnu/libpurple.so npm run test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | build/ 4 | appservice/config.yaml 5 | appservice/purple-registration.yaml 6 | 7 | lib/ 8 | 9 | #Qt Creator 10 | *.creator* 11 | *.files 12 | *.includes 13 | *.config 14 | 15 | deps/libpurple 16 | deps/pidgin-* 17 | deps/*.tar* 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-purple 2 | 3 | NodeJS N-API bindings for libpurple 2.13.0. 4 | 5 | You will need to install these packages in order to compile 6 | 7 | - libpurple-bin (2.13.0) 8 | - libpurple-dev (2.13.0) 9 | - libglib2.0-dev 10 | - python3 11 | 12 | # Contributing 13 | 14 | Bugs against `node-purple` are tracked on [matrix-bifrost](https://github.com/matrix-org/matrix-bifrost/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Anode-purple) 15 | 16 | # Usage 17 | 18 | TODO 19 | 20 | # Help 21 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "module", 5 | "dependencies": [], 6 | "sources": [ 7 | "./src/module.c", 8 | "./src/helper.c", 9 | "./src/eventloop.c", 10 | "./src/signalling.c", 11 | "./src/messaging.c", 12 | "./src/bindings/b_core.c", 13 | "./src/bindings/b_plugins.c", 14 | "./src/bindings/b_accounts.c", 15 | "./src/bindings/b_buddy.c", 16 | "./src/bindings/b_notify.c", 17 | "./src/napi_helpers.c" 18 | ], 19 | "include_dirs": [ 20 | " 4 | 5 | export as namespace libpurple; 6 | 7 | /* Types */ 8 | export type Event = { 9 | eventName: string; 10 | } 11 | 12 | export type SetupArgs = { 13 | debugEnabled?: number; 14 | userDir?: string; 15 | pluginDir?: string; 16 | } 17 | 18 | export type Account = { 19 | handle: External; 20 | username: string; 21 | protocol_id: string; 22 | password?: string; 23 | user_info?: string; 24 | buddy_icon_path?: string; 25 | settings?: Record; 26 | } 27 | 28 | export type Protocol = { 29 | name: string; 30 | id: string; 31 | homepage?: string; 32 | summary?: string; 33 | } 34 | 35 | export type StatusType = { 36 | id: string; 37 | name: string; 38 | saveable: boolean; 39 | user_settable: boolean; 40 | independent: boolean; 41 | }; 42 | 43 | export type Buddy = { 44 | name: string; 45 | icon_path?: string; 46 | nick?: string; 47 | }; 48 | 49 | export type Conversation = { 50 | handle: External; 51 | name: string; 52 | } 53 | 54 | export enum TypingState { 55 | NotTyping = 0, 56 | Typing = 1, 57 | Typed = 2, 58 | } 59 | 60 | /* Sub-modules */ 61 | export class core { 62 | /** 63 | * Get the version of purple in use. 64 | */ 65 | static get_version(): string; 66 | /** 67 | * Start the purple core. 68 | * @deprecated Use `helper.setupPurple` which does this and more. 69 | */ 70 | static init(); 71 | /** 72 | * Stps the purple core. 73 | */ 74 | static quit(); 75 | } 76 | 77 | export class debug { 78 | // Nothing yet 79 | } 80 | 81 | export class helper { 82 | /** 83 | * Configure purple to start without any UI features. A configuration object 84 | * should be provided. 85 | * @param opts 86 | */ 87 | static setupPurple(opts: SetupArgs); 88 | static pollEvents(): Event[]; 89 | } 90 | 91 | export class plugins { 92 | static get_protocols(): Protocol[]; 93 | } 94 | 95 | export class accounts { 96 | static new(name: string, pluginId: string, password?: string): Account; 97 | /** 98 | * Configure an account with extra options. 99 | * @param handle The account external Handle. 100 | * @param config A configuration object. Numbers must be integers 101 | * @throws If the configuration object contains invalid types, or if the handle is invalid. 102 | */ 103 | static configure(handle: External, config: Record); 104 | /** 105 | * Get all accounts configured on the purple instance. 106 | */ 107 | static get_all(): Account[]; 108 | static find(name: string, pluginId: string): Account; 109 | static get_enabled(handle: External): boolean; 110 | static set_enabled(handle: External, enable: boolean); 111 | static connect(handle: External); 112 | static disconnect(handle: External); 113 | static is_connected(handle: External): boolean; 114 | static is_connecting(handle: External): boolean; 115 | static is_disconnected(handle: External): boolean; 116 | static is_disconnected(handle: External): boolean; 117 | static get_status_types(handle: External): StatusType[]; 118 | static set_status(handle: External, statusId: string, active: boolean); 119 | } 120 | 121 | export class messaging { 122 | static sendIM(handle: External, name: string, body: string); 123 | static setIMTypingState(handle: External, name: string, state: TypingState); 124 | static sendChat(handle: External, name: string, body: string); 125 | static chatParams(handle: External, protocol: string); 126 | static joinChat(handle: External, components: {[key: string]:string;}); 127 | static rejectChat(handle: External, components: {[key: string]:string;}); 128 | static getBuddyFromConv(handle: External, buddyName: string); 129 | static getNickForChat(chatHandle: External); 130 | static findConversation(acct: External, name: string): Conversation; 131 | } 132 | 133 | export class notify { 134 | static get_user_info(handle: External, who: string); 135 | } 136 | 137 | export class buddy { 138 | /** 139 | * Find a buddy in the buddylist. 140 | * @param handle Account Handle 141 | * @param name [description] 142 | * @return [description] 143 | */ 144 | static find(handle: External, name: string): Buddy; 145 | } 146 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require(require('path').join(__dirname, "lib", "module.node")); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-purple", 3 | "version": "0.0.2", 4 | "description": "N-API bindings for libpurple", 5 | "main": "main.js", 6 | "typings": "index.d.ts", 7 | "author": "Half-Shot ", 8 | "license": "Apache-2.0", 9 | "dependencies": { 10 | "node-gyp": "^10.0.1" 11 | }, 12 | "devDependencies": { 13 | "@types/chai": "^4.2.19", 14 | "@types/mocha": "^8.2.2", 15 | "@types/node": "^14", 16 | "chai": "^4.3.4", 17 | "mocha": "^9.0.1", 18 | "ts-node": "^10.9.2", 19 | "typescript": "^4.3.4" 20 | }, 21 | "scripts": { 22 | "rebuild": "node-gyp build", 23 | "build": "node-gyp build", 24 | "test": "mocha --require ts-node/register spec/**/*.spec.ts" 25 | }, 26 | "binary": { 27 | "module_name": "module", 28 | "module_path": "./lib", 29 | "host": "foo" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spec/core.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { core } from ".."; 3 | 4 | // This breaks if we upgrade to a later version of purple, but that's quite rare. 5 | const MAJOR_LIBPURPLE_VERSION = "2."; 6 | 7 | describe("Core", () => { 8 | it("can get the version of purple in use", () => { 9 | expect(core.get_version().startsWith(MAJOR_LIBPURPLE_VERSION)); 10 | }); 11 | }); -------------------------------------------------------------------------------- /spec/helper.ts: -------------------------------------------------------------------------------- 1 | import { core, helper } from ".."; 2 | import { mkdtemp, rm } from "fs/promises"; 3 | import { join } from "path"; 4 | import { tmpdir } from "os"; 5 | 6 | const EXPECTED_PLUGIN_DIR = "/usr/lib/purple-2"; 7 | 8 | export async function setupPurpleEnv() { 9 | const dir = await mkdtemp(join(tmpdir(), 'node-purple-test-')); 10 | helper.setupPurple({ 11 | userDir: dir, 12 | pluginDir: EXPECTED_PLUGIN_DIR, 13 | debugEnabled: process.env.PURPLE_LOGGING ? 1 : 0, 14 | }); 15 | return dir; 16 | } 17 | 18 | export function tearDownPurpleEnv(dir) { 19 | core.quit(); 20 | rm(dir, { recursive: true, force: true }); 21 | } -------------------------------------------------------------------------------- /spec/plugins.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { plugins } from ".."; 3 | import { setupPurpleEnv, tearDownPurpleEnv } from "./helper"; 4 | 5 | describe("Plugins", () => { 6 | let dir; 7 | beforeEach(async () => { 8 | dir = await setupPurpleEnv(); 9 | }) 10 | afterEach(() => { 11 | tearDownPurpleEnv(dir); 12 | }); 13 | it("can get a list of supported protocols", () => { 14 | const protocols = plugins.get_protocols(); 15 | expect(protocols).has.length.greaterThan(0); 16 | }); 17 | }); -------------------------------------------------------------------------------- /src/bindings/b_accounts.c: -------------------------------------------------------------------------------- 1 | #include "b_accounts.h" 2 | 3 | // TODO: Taken from libpurple/account.c 4 | typedef struct 5 | { 6 | PurplePrefType type; 7 | 8 | char *ui; 9 | 10 | union 11 | { 12 | int integer; 13 | char *string; 14 | gboolean boolean; 15 | 16 | } value; 17 | 18 | } PurpleAccountSetting; 19 | 20 | napi_status account_settings_to_object(napi_env env, napi_value value, GHashTable *hashTable) { 21 | napi_status status = napi_generic_failure; 22 | GHashTableIter iter; 23 | gpointer key, val; 24 | napi_value jkey, jvalue; 25 | 26 | g_hash_table_iter_init (&iter, hashTable); 27 | while (g_hash_table_iter_next (&iter, &key, &val)) 28 | { 29 | char* skey = (char*)key; 30 | PurpleAccountSetting* sval = (PurpleAccountSetting*) val; 31 | 32 | if (sval->type == PURPLE_PREF_STRING) { 33 | status = napi_create_string_utf8(env, sval->value.string, NAPI_AUTO_LENGTH, &jvalue); 34 | if (status != napi_ok) return status; 35 | } else if (sval->type == PURPLE_PREF_BOOLEAN) { 36 | status = napi_get_boolean(env, sval->value.boolean, &jvalue); 37 | if (status != napi_ok) return status; 38 | } else if (sval->type == PURPLE_PREF_INT) { 39 | status = napi_create_int32(env, sval->value.integer, &jvalue); 40 | if (status != napi_ok) return status; 41 | } else { 42 | status = napi_get_undefined(env, &jvalue); 43 | if (status != napi_ok) return status; 44 | } 45 | 46 | status = napi_create_string_utf8(env, skey, NAPI_AUTO_LENGTH, &jkey); 47 | if (status != napi_ok) return status; 48 | status = napi_set_property(env, value, jkey, jvalue); 49 | if (status != napi_ok) return status; 50 | } 51 | 52 | return status; 53 | } 54 | 55 | napi_value nprpl_account_create(napi_env env, PurpleAccount *acct){ 56 | napi_value obj; 57 | napi_value value; 58 | napi_create_object(env, &obj); 59 | /* username */ 60 | napi_create_string_utf8(env, acct->username, NAPI_AUTO_LENGTH, &value); 61 | napi_set_named_property(env, obj, "username", value); 62 | /* alias */ 63 | if (acct->alias != NULL) { 64 | napi_create_string_utf8(env, acct->alias, NAPI_AUTO_LENGTH, &value); 65 | napi_set_named_property(env, obj, "alias", value); 66 | } 67 | 68 | /* password */ 69 | if (acct->password != NULL) { 70 | napi_create_string_utf8(env, acct->password, NAPI_AUTO_LENGTH, &value); 71 | napi_set_named_property(env, obj, "password", value); 72 | } 73 | /* user_info */ 74 | if (acct->user_info != NULL) { 75 | napi_create_string_utf8(env, acct->user_info, NAPI_AUTO_LENGTH, &value); 76 | napi_set_named_property(env, obj, "user_info", value); 77 | } 78 | /* buddy_icon_path */ 79 | if (acct->buddy_icon_path != NULL) { 80 | napi_create_string_utf8(env, acct->buddy_icon_path, NAPI_AUTO_LENGTH, &value); 81 | napi_set_named_property(env, obj, "buddy_icon_path", value); 82 | } 83 | /* settings */ 84 | if (acct->settings != NULL) { 85 | napi_create_object(env, &value); 86 | account_settings_to_object(env, value, acct->settings); 87 | napi_set_named_property(env, obj, "settings", value); 88 | } 89 | /* protocol_id */ 90 | napi_create_string_utf8(env, acct->protocol_id, NAPI_AUTO_LENGTH, &value); 91 | napi_set_named_property(env, obj, "protocol_id", value); 92 | 93 | /* handle */ 94 | napi_create_external(env, acct, NULL, NULL, &value); 95 | napi_set_named_property(env, obj, "handle", value); 96 | 97 | return obj; 98 | } 99 | 100 | napi_value create_object_from_statustype(napi_env env, PurpleStatusType* statustype) { 101 | napi_value obj; 102 | napi_value value; 103 | napi_create_object(env, &obj); 104 | 105 | /* id */ 106 | napi_create_string_utf8(env, purple_status_type_get_id(statustype), NAPI_AUTO_LENGTH, &value); 107 | napi_set_named_property(env, obj, "id", value); 108 | 109 | /* name */ 110 | napi_create_string_utf8(env, purple_status_type_get_name(statustype), NAPI_AUTO_LENGTH, &value); 111 | napi_set_named_property(env, obj, "name", value); 112 | 113 | /* saveable */ 114 | napi_get_boolean(env, purple_status_type_is_saveable(statustype), &value); 115 | napi_set_named_property(env, obj, "saveable", value); 116 | 117 | /* user_settable */ 118 | napi_get_boolean(env, purple_status_type_is_user_settable(statustype), &value); 119 | napi_set_named_property(env, obj, "user_settable", value); 120 | 121 | /* independent */ 122 | napi_get_boolean(env, purple_status_type_is_independent(statustype), &value); 123 | napi_set_named_property(env, obj, "independent", value); 124 | 125 | return obj; 126 | } 127 | 128 | PurpleAccount* __getacct(napi_env env, napi_callback_info info) { 129 | PurpleAccount *account; 130 | size_t argc = 1; 131 | napi_value opt; 132 | napi_get_cb_info(env, info, &argc, &opt, NULL, NULL); 133 | if (argc < 1) { 134 | THROW(env, NULL, "takes one argument", NULL); 135 | } 136 | napi_get_value_external(env, opt, (void*)&account); 137 | return account; 138 | } 139 | 140 | napi_value _purple_accounts_new(napi_env env, napi_callback_info info) { 141 | napi_value n_out; 142 | size_t argc = 3; 143 | napi_value opts[argc]; 144 | 145 | napi_get_cb_info(env, info, &argc, opts, NULL, NULL); 146 | if (argc == 0) { 147 | THROW(env, NULL, "new takes two arguments", NULL); 148 | } 149 | 150 | char* name = napi_help_strfromval(env, opts[0]); 151 | char* prpl = napi_help_strfromval(env, opts[1]); 152 | 153 | PurpleAccount *account = purple_account_new(name, prpl); 154 | if (account != NULL) { 155 | purple_accounts_add(account); 156 | } 157 | if (argc == 3) { 158 | char* password = napi_help_strfromval(env, opts[2]); 159 | purple_account_set_remember_password(account, TRUE); 160 | purple_account_set_password(account, password); 161 | free(password); 162 | } 163 | n_out = nprpl_account_create(env, account); 164 | 165 | free(name); 166 | free(prpl); 167 | 168 | return n_out; 169 | } 170 | 171 | /** 172 | * Configure an account with a given object, calling purple_account_set_* for 173 | * string, int and boolean value types. 174 | * This will throw if the account does not exist, or if the configuration object was invalid. 175 | * This operation MAY partially succeed. 176 | */ 177 | napi_value _purple_account_configure(napi_env env, napi_callback_info info) { 178 | size_t argc = 2; 179 | napi_value opts[2]; 180 | PurpleAccount *account; 181 | 182 | if (napi_get_cb_info(env, info, &argc, opts, NULL, NULL) != napi_ok) { 183 | THROW(env, NULL, "napi_get_cb_info failed", NULL); 184 | } 185 | if (argc < 2) { 186 | THROW(env, NULL, "get_enabled takes two arguments", NULL); 187 | } 188 | 189 | napi_get_value_external(env, opts[0], (void*)&account); 190 | 191 | napi_value config_object = opts[1]; 192 | 193 | napi_value nComponentNames; 194 | napi_value jkey; 195 | napi_value jvalue; 196 | napi_valuetype type; 197 | char* key; 198 | 199 | unsigned int length; 200 | if (napi_get_property_names(env, config_object, &nComponentNames) != napi_ok || 201 | napi_get_array_length(env, nComponentNames, &length) != napi_ok) { 202 | THROW(env, NULL, "passed config option not an object", NULL); 203 | } 204 | for (unsigned int i = 0; i < length; i++) { 205 | napi_get_element(env, nComponentNames, i, &jkey); 206 | napi_get_property(env, config_object, jkey, &jvalue); 207 | napi_typeof(env, jvalue, &type); 208 | key = napi_help_strfromval(env, jkey); 209 | char error[256]; 210 | switch (type) 211 | { 212 | case napi_string: 213 | key = napi_help_strfromval(env, jkey); 214 | char* svalue = napi_help_strfromval(env, jvalue); 215 | if (strcmp(key, "password") == 0) { 216 | purple_account_set_password(account, svalue); 217 | } else if (strcmp(key, "username") == 0) { 218 | purple_account_set_username(account, svalue); 219 | } else { 220 | purple_account_set_string(account, key, svalue); 221 | } 222 | break; 223 | case napi_bigint: 224 | key = napi_help_strfromval(env, jkey); 225 | int ivalue; 226 | // Technically this could be larger, but it's highly unlikely we'd need larger. 227 | if (napi_get_value_int32(env, jvalue, &ivalue) == napi_ok) { 228 | purple_account_set_int(account, key, ivalue); 229 | } else { 230 | THROW(env, NULL, "Could not cooerce bitint value into int32", NULL); 231 | } 232 | break; 233 | case napi_boolean: 234 | key = napi_help_strfromval(env, jkey); 235 | bool bvalue; 236 | if (napi_get_value_bool(env, jvalue, &bvalue) == napi_ok) { 237 | purple_account_set_bool(account, key, bvalue); 238 | } else { 239 | THROW(env, NULL, "Could not cooerce JS boolean value into boolean", NULL); 240 | } 241 | break; 242 | default: 243 | sprintf(error, "Cannot handle type for %s", key); 244 | THROW(env, NULL, error, NULL); 245 | } 246 | free(key); 247 | } 248 | 249 | return NULL; 250 | } 251 | 252 | napi_value _purple_accounts_find(napi_env env, napi_callback_info info) { 253 | napi_value n_out; 254 | size_t argc = 2; 255 | napi_value opts[2]; 256 | 257 | napi_get_cb_info(env, info, &argc, opts, NULL, NULL); 258 | if (argc == 0) { 259 | THROW(env, NULL, "find takes two arguments", NULL); 260 | } 261 | 262 | char* name = napi_help_strfromval(env, opts[0]); 263 | char* prpl = napi_help_strfromval(env, opts[1]); 264 | 265 | PurpleAccount *account = purple_accounts_find(name, prpl); 266 | if (account == false) { 267 | napi_get_undefined(env, &n_out); 268 | } else { 269 | n_out = nprpl_account_create(env, account); 270 | } 271 | free(name); 272 | free(prpl); 273 | 274 | return n_out; 275 | } 276 | 277 | napi_value _purple_accounts_get_all(napi_env env, napi_callback_info info) { 278 | napi_value account_array; 279 | napi_create_array(env, &account_array); 280 | GList* accounts = purple_accounts_get_all(); 281 | GList* l; 282 | uint32_t i = 0; 283 | for (l = accounts; l != NULL; l = l->next) 284 | { 285 | PurpleAccount *account = (PurpleAccount*)l->data; 286 | napi_value obj = nprpl_account_create(env, account); 287 | napi_set_element(env, account_array, i, obj); 288 | i++; 289 | } 290 | return account_array; 291 | } 292 | 293 | napi_value _purple_accounts_get_enabled(napi_env env, napi_callback_info info) { 294 | napi_value n_out; 295 | size_t argc = 1; 296 | napi_value opt; 297 | PurpleAccount *account; 298 | napi_get_cb_info(env, info, &argc, &opt, NULL, NULL); 299 | if (argc < 1) { 300 | THROW(env, NULL, "get_enabled takes one argument", NULL); 301 | } 302 | 303 | napi_get_value_external(env, opt, &account); 304 | gboolean enabled = purple_account_get_enabled(account, STR_PURPLE_UI); 305 | napi_get_boolean(env, enabled, &n_out); 306 | return n_out; 307 | } 308 | 309 | napi_value _purple_accounts_set_enabled(napi_env env, napi_callback_info info) { 310 | size_t argc = 2; 311 | napi_value opts[2]; 312 | PurpleAccount *account; 313 | napi_get_cb_info(env, info, &argc, opts, NULL, NULL); 314 | if (argc < 2) { 315 | THROW(env, NULL, "set_enabled takes two arguments", NULL); 316 | } 317 | 318 | napi_get_value_external(env, opts[0], &account); 319 | bool enable; 320 | napi_get_value_bool(env, opts[1], &enable); 321 | // Gboolean nonsense 322 | purple_account_set_enabled(account, STR_PURPLE_UI, enable ? TRUE : FALSE); 323 | 324 | return NULL; 325 | } 326 | 327 | napi_value _purple_accounts_connect(napi_env env, napi_callback_info info) { 328 | PurpleAccount *account = __getacct(env, info); 329 | purple_account_connect(account); 330 | 331 | return NULL; 332 | } 333 | 334 | napi_value _purple_accounts_disconnect(napi_env env, napi_callback_info info) { 335 | PurpleAccount *account = __getacct(env, info); 336 | purple_account_disconnect(account); 337 | 338 | return NULL; 339 | } 340 | 341 | napi_value _purple_account_is_connected(napi_env env, napi_callback_info info) { 342 | PurpleAccount *account = __getacct(env, info); 343 | bool res = purple_account_is_connected(account); 344 | napi_value jres; 345 | napi_get_boolean(env, res, &jres); 346 | return jres; 347 | } 348 | 349 | napi_value _purple_account_is_connecting(napi_env env, napi_callback_info info) { 350 | PurpleAccount *account = __getacct(env, info); 351 | bool res = purple_account_is_connecting(account); 352 | napi_value jres; 353 | napi_get_boolean(env, res, &jres); 354 | return jres; 355 | } 356 | 357 | napi_value _purple_account_is_disconnected(napi_env env, napi_callback_info info) { 358 | PurpleAccount *account = __getacct(env, info); 359 | bool res = purple_account_is_disconnected(account); 360 | napi_value jres; 361 | napi_get_boolean(env, res, &jres); 362 | return jres; 363 | } 364 | 365 | napi_value _purple_account_get_status_types(napi_env env, napi_callback_info info) { 366 | napi_value status_array; 367 | GList* status_types; 368 | PurpleAccount *account = __getacct(env, info); 369 | napi_create_array(env, &status_array); 370 | 371 | status_types = purple_account_get_status_types(account); 372 | GList* l; 373 | uint32_t i = 0; 374 | for (l = status_types; l != NULL; l = l->next) 375 | { 376 | PurpleStatusType *type = (PurpleStatusType*)l->data; 377 | napi_value obj = create_object_from_statustype(env, type); 378 | napi_set_element(env, status_array, i, obj); 379 | i++; 380 | } 381 | return status_array; 382 | } 383 | 384 | napi_value _purple_account_set_status(napi_env env, napi_callback_info info) { 385 | PurpleAccount *account; 386 | char* id; 387 | bool active; 388 | size_t argc = 3; 389 | napi_value opt[3]; 390 | napi_get_cb_info(env, info, &argc, opt, NULL, NULL); 391 | if (argc < 3) { 392 | THROW(env, NULL, "takes three arguments", NULL); 393 | } 394 | 395 | napi_get_value_external(env, opt[0], (void*)&account); 396 | id = napi_help_strfromval(env, opt[1]); 397 | napi_get_value_bool(env, opt[2], &active); 398 | 399 | purple_account_set_status(account, id, active, NULL); 400 | 401 | return NULL; 402 | } 403 | 404 | void accounts_bind_node(napi_env env, napi_value root) { 405 | napi_value ns_accounts; 406 | napi_value _func; 407 | napi_create_object(env, &ns_accounts); 408 | 409 | napi_create_function(env, "new", NAPI_AUTO_LENGTH, _purple_accounts_new, NULL, &_func); 410 | napi_set_named_property(env, ns_accounts, "new", _func); 411 | 412 | napi_create_function(env, "configure", NAPI_AUTO_LENGTH, _purple_account_configure, NULL, &_func); 413 | napi_set_named_property(env, ns_accounts, "configure", _func); 414 | 415 | napi_create_function(env, "find", NAPI_AUTO_LENGTH, _purple_accounts_find, NULL, &_func); 416 | napi_set_named_property(env, ns_accounts, "find", _func); 417 | 418 | napi_create_function(env, "get_all", NAPI_AUTO_LENGTH, _purple_accounts_get_all, NULL, &_func); 419 | napi_set_named_property(env, ns_accounts, "get_all", _func); 420 | 421 | napi_create_function(env, "get_enabled", NAPI_AUTO_LENGTH, _purple_accounts_get_enabled, NULL, &_func); 422 | napi_set_named_property(env, ns_accounts, "get_enabled", _func); 423 | 424 | napi_create_function(env, "set_enabled", NAPI_AUTO_LENGTH, _purple_accounts_set_enabled, NULL, &_func); 425 | napi_set_named_property(env, ns_accounts, "set_enabled", _func); 426 | 427 | napi_create_function(env, "connect", NAPI_AUTO_LENGTH, _purple_accounts_connect, NULL, &_func); 428 | napi_set_named_property(env, ns_accounts, "connect", _func); 429 | 430 | napi_create_function(env, "disconnect", NAPI_AUTO_LENGTH, _purple_accounts_disconnect, NULL, &_func); 431 | napi_set_named_property(env, ns_accounts, "disconnect", _func); 432 | 433 | napi_create_function(env, "is_connected", NAPI_AUTO_LENGTH, _purple_account_is_connected, NULL, &_func); 434 | napi_set_named_property(env, ns_accounts, "is_connected", _func); 435 | 436 | napi_create_function(env, "is_connecting", NAPI_AUTO_LENGTH, _purple_account_is_connecting, NULL, &_func); 437 | napi_set_named_property(env, ns_accounts, "is_connecting", _func); 438 | 439 | napi_create_function(env, "is_disconnected", NAPI_AUTO_LENGTH, _purple_account_is_disconnected, NULL, &_func); 440 | napi_set_named_property(env, ns_accounts, "is_disconnected", _func); 441 | 442 | napi_create_function(env, "get_status_types", NAPI_AUTO_LENGTH, _purple_account_get_status_types, NULL, &_func); 443 | napi_set_named_property(env, ns_accounts, "get_status_types", _func); 444 | 445 | napi_create_function(env, "set_status", NAPI_AUTO_LENGTH, _purple_account_set_status, NULL, &_func); 446 | napi_set_named_property(env, ns_accounts, "set_status", _func); 447 | 448 | napi_set_named_property(env, root, "accounts", ns_accounts); 449 | } 450 | -------------------------------------------------------------------------------- /src/bindings/b_accounts.h: -------------------------------------------------------------------------------- 1 | #ifndef ACCOUNTS_H_INCLUDED 2 | #define ACCOUNTS_H_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "../helper.h" 9 | #include "../napi_helpers.h" 10 | 11 | napi_value nprpl_account_create(napi_env env, PurpleAccount *acct); 12 | void accounts_bind_node(napi_env env,napi_value root); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /src/bindings/b_buddy.c: -------------------------------------------------------------------------------- 1 | #include "b_buddy.h" 2 | #include "helper.h" 3 | 4 | napi_value nprpl_buddy_create(napi_env env, PurpleBuddy* buddy) { 5 | napi_value obj; 6 | napi_value value; 7 | napi_create_object(env, &obj); 8 | const char* icon_filepath; 9 | const char* nick; 10 | /* name */ 11 | napi_create_string_utf8(env, purple_buddy_get_name(buddy), NAPI_AUTO_LENGTH, &value); 12 | napi_set_named_property(env, obj, "name", value); 13 | 14 | /* icon */ 15 | PurpleBuddyIcon* icon = purple_buddy_get_icon(buddy); 16 | 17 | if (icon) { 18 | icon_filepath = purple_buddy_icon_get_full_path(icon); 19 | } else { 20 | // Attempt to load it from the store. 21 | icon_filepath = g_build_filename( 22 | purple_buddy_icons_get_cache_dir(), 23 | purple_blist_node_get_string((PurpleBlistNode*)buddy, "buddy_icon"), 24 | NULL 25 | ); 26 | } 27 | 28 | if (icon_filepath != NULL) { 29 | napi_create_string_utf8(env, icon_filepath, NAPI_AUTO_LENGTH, &value); 30 | napi_set_named_property(env, obj, "icon_path", value); 31 | } 32 | 33 | /* status */ 34 | PurpleStatus* status = purple_presence_get_active_status( 35 | purple_buddy_get_presence(buddy)); 36 | if (status != NULL) { 37 | napi_create_string_utf8(env, purple_status_get_id(status), NAPI_AUTO_LENGTH, &value); 38 | napi_set_named_property(env, obj, "status_id", value); 39 | 40 | nick = purple_status_get_attr_string(status, "nick"); 41 | if (nick == NULL) { 42 | // We might have saved the nick previously 43 | nick = purple_blist_node_get_string((PurpleBlistNode*)buddy, "servernick"); 44 | } 45 | if (nick != NULL) { 46 | napi_create_string_utf8(env, nick, NAPI_AUTO_LENGTH, &value); 47 | napi_set_named_property(env, obj, "nick", value); 48 | } 49 | } 50 | 51 | 52 | return obj; 53 | } 54 | 55 | 56 | napi_value _buddy_find(napi_env env, napi_callback_info info) { 57 | PurpleAccount *account; 58 | PurpleBuddy* buddy; 59 | size_t argc = 2; 60 | napi_value opt[2]; 61 | napi_value n_out; 62 | napi_get_cb_info(env, info, &argc, opt, NULL, NULL); 63 | if (argc < 2) { 64 | THROW(env, NULL, "takes two arguments", NULL); 65 | } 66 | napi_get_value_external(env, opt[0], (void*)&account); 67 | char* name = napi_help_strfromval(env, opt[1]); 68 | 69 | buddy = purple_find_buddy(account, name); 70 | free(name); 71 | if (buddy != NULL) { 72 | n_out = nprpl_buddy_create(env, buddy); 73 | } else { 74 | napi_get_undefined(env, &n_out); 75 | } 76 | return n_out; 77 | } 78 | 79 | void buddy_bind_node(napi_env env, napi_value root) { 80 | napi_value namespace; 81 | napi_value func; 82 | napi_create_object(env, &namespace); 83 | 84 | napi_create_function(env, NULL, 0, _buddy_find, NULL, &func); 85 | napi_set_named_property(env, namespace, "find", func); 86 | 87 | napi_set_named_property(env, root, "buddy", namespace); 88 | } 89 | -------------------------------------------------------------------------------- /src/bindings/b_buddy.h: -------------------------------------------------------------------------------- 1 | #ifndef BUDDY_H_INCLUDED 2 | #define BUDDY_H_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "../napi_helpers.h" 10 | 11 | void buddy_bind_node(napi_env env,napi_value root); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/bindings/b_core.c: -------------------------------------------------------------------------------- 1 | #include "b_core.h" 2 | 3 | napi_value _purple_core_get_version(napi_env env, napi_callback_info info) { 4 | const char *version; 5 | napi_status status; 6 | napi_value node_version; 7 | version = purple_core_get_version(); 8 | 9 | status = napi_create_string_utf8(env, version, NAPI_AUTO_LENGTH, &node_version); 10 | 11 | if (status != napi_ok) { 12 | THROW(env, NULL, "Unable to create return value", NULL); 13 | } 14 | 15 | return node_version; 16 | } 17 | 18 | napi_value _purple_core_init(napi_env env, napi_callback_info info) { 19 | gboolean result; 20 | napi_value n_result; 21 | result = purple_core_init("matrix-bridge"); 22 | 23 | if (napi_get_boolean(env, result, &n_result) != napi_ok) { 24 | THROW(env, NULL, "Unable to create return value", NULL); 25 | } 26 | 27 | return n_result; 28 | } 29 | 30 | napi_value _purple_core_quit(napi_env env, napi_callback_info info) { 31 | purple_core_quit(); 32 | napi_value n_undef; 33 | napi_get_undefined(env, &n_undef); 34 | return n_undef; 35 | } 36 | -------------------------------------------------------------------------------- /src/bindings/b_core.h: -------------------------------------------------------------------------------- 1 | #ifndef CORE_H_INCLUDED 2 | #define CORE_H_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "helper.h" 9 | napi_value _purple_core_get_version(napi_env env, napi_callback_info info); 10 | napi_value _purple_core_init(napi_env env, napi_callback_info info); 11 | napi_value _purple_core_quit(napi_env env, napi_callback_info info); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/bindings/b_notify.c: -------------------------------------------------------------------------------- 1 | #include "b_notify.h" 2 | #include "helper.h" 3 | 4 | napi_value get_user_info(napi_env env, napi_callback_info info) { 5 | PurpleAccount *account; 6 | size_t argc = 2; 7 | napi_value opt[2]; 8 | napi_get_cb_info(env, info, &argc, opt, NULL, NULL); 9 | if (argc < 2) { 10 | THROW(env, NULL, "takes two arguments", NULL); 11 | } 12 | napi_get_value_external(env, opt[0], (void*)&account); 13 | PurpleConnection* conn = purple_account_get_connection(account); 14 | char* who = napi_help_strfromval(env, opt[1]); 15 | serv_get_info(conn, who); 16 | 17 | return NULL; 18 | } 19 | 20 | void notify_bind_node(napi_env env,napi_value root) { 21 | napi_value namespace; 22 | napi_value func; 23 | napi_create_object(env, &namespace); 24 | 25 | napi_create_function(env, NULL, 0, get_user_info, NULL, &func); 26 | napi_set_named_property(env, namespace, "get_user_info", func); 27 | 28 | napi_set_named_property(env, root, "notify", namespace); 29 | } 30 | -------------------------------------------------------------------------------- /src/bindings/b_notify.h: -------------------------------------------------------------------------------- 1 | #ifndef NOTIFY_H_INCLUDED 2 | #define NOTIFY_H_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "../napi_helpers.h" 10 | 11 | void notify_bind_node(napi_env env,napi_value root); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/bindings/b_plugins.c: -------------------------------------------------------------------------------- 1 | #include "b_plugins.h" 2 | 3 | 4 | napi_value create_object_from_plugin(napi_env env, PurplePlugin *plugin){ 5 | napi_value obj; 6 | napi_value value; 7 | napi_create_object(env, &obj); 8 | /* info->name */ 9 | napi_create_string_utf8(env, plugin->info->name, NAPI_AUTO_LENGTH, &value); 10 | napi_set_named_property(env, obj, "name", value); 11 | /* info->id */ 12 | napi_create_string_utf8(env, plugin->info->id, NAPI_AUTO_LENGTH, &value); 13 | napi_set_named_property(env, obj, "id", value); 14 | /* info->homepage */ 15 | napi_create_string_utf8(env, plugin->info->homepage, NAPI_AUTO_LENGTH, &value); 16 | napi_set_named_property(env, obj, "homepage", value); 17 | /* info->summary */ 18 | napi_create_string_utf8(env, plugin->info->summary, NAPI_AUTO_LENGTH, &value); 19 | napi_set_named_property(env, obj, "summary", value); 20 | 21 | return obj; 22 | } 23 | 24 | napi_value _purple_plugins_get_protocols(napi_env env, napi_callback_info info) { 25 | napi_value protocol_array; 26 | napi_create_array(env, &protocol_array); 27 | GList* plugins = purple_plugins_get_protocols(); 28 | GList* l; 29 | uint32_t i = 0; 30 | for (l = plugins; l != NULL; l = l->next) 31 | { 32 | PurplePlugin *plugin = (PurplePlugin*)l->data; 33 | napi_value obj = create_object_from_plugin(env, plugin); 34 | napi_set_element(env, protocol_array, i, obj); 35 | i++; 36 | } 37 | return protocol_array; 38 | } 39 | -------------------------------------------------------------------------------- /src/bindings/b_plugins.h: -------------------------------------------------------------------------------- 1 | #ifndef PLUGINS_H_INCLUDED 2 | #define PLUGINS_H_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | napi_value _purple_plugins_get_protocols(napi_env env, napi_callback_info info); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /src/eventloop.c: -------------------------------------------------------------------------------- 1 | #include "eventloop.h" 2 | #include "helper.h" 3 | 4 | /* 5 | Horrible stiching together of libuv's loop for purple. 6 | This works by getting the node env's loop and adding our own 7 | timers and polls to it. 8 | */ 9 | 10 | // Structure for timers 11 | // One uv_timer_t corresponds to one s_evLoopTimer 12 | typedef struct { 13 | uv_timer_t* handle; 14 | GSourceFunc function; 15 | gpointer data; 16 | } s_evLoopTimer; 17 | 18 | /** 19 | * Structure used to map one uv_poll_t to many events. 20 | * When the events list is empty, this should be culled. 21 | */ 22 | typedef struct { 23 | uv_poll_t* handle; 24 | int fd; 25 | int cond; 26 | // s_evLoopInputEvent 27 | GList* events; 28 | } s_evLoopInput; 29 | 30 | typedef struct { 31 | PurpleInputFunction func; 32 | gpointer user_data; 33 | int events; 34 | s_evLoopInput* parent; 35 | } s_evLoopInputEvent; 36 | 37 | // Global state for the eventloop. 38 | typedef struct { 39 | uv_loop_t* loop; 40 | // fd -> s_evLoopInput 41 | GHashTable* inputs; 42 | } s_evLoopState; 43 | 44 | void call_callback(uv_timer_t* handle); 45 | 46 | static s_evLoopState evLoopState; 47 | 48 | /** 49 | * Should create a callback timer with an interval measured in 50 | * milliseconds. The supplied @a function should be called every @a 51 | * interval seconds until it returns @c FALSE, after which it should not 52 | * be called again. 53 | * 54 | * Analogous to g_timeout_add in glib. 55 | * 56 | * Note: On Win32, this function may be called from a thread other than 57 | * the libpurple thread. You should make sure to detect this situation 58 | * and to only call "function" from the libpurple thread. 59 | * 60 | * @param interval the interval in milliseconds between calls 61 | * to @a function. 62 | * @param data arbitrary data to be passed to @a function at each 63 | * call. 64 | * @todo Who is responsible for freeing @a data? 65 | * 66 | * @return a handle for the timeout, which can be passed to 67 | * #timeout_remove. 68 | * 69 | * @see purple_timeout_add 70 | **/ 71 | guint timeout_add (guint interval, GSourceFunc function, gpointer data) { 72 | s_evLoopTimer *timer = malloc(sizeof(s_evLoopTimer)); 73 | uv_timer_t *handle = malloc(sizeof(uv_timer_t)); 74 | uv_timer_init(evLoopState.loop, handle); 75 | timer->handle = handle; 76 | timer->function = function; 77 | timer->data = data; 78 | uv_handle_set_data((uv_handle_t*)handle, timer); 79 | uv_timer_start(handle, call_callback, interval, 0); 80 | return GPOINTER_TO_UINT(timer); 81 | } 82 | 83 | /** 84 | * If implemented, should create a callback timer with an interval 85 | * measured in seconds. Analogous to g_timeout_add_seconds in glib. 86 | * 87 | * This allows UIs to group timers for better power efficiency. For 88 | * this reason, @a interval may be rounded by up to a second. 89 | * 90 | * Implementation of this UI op is optional. If it's not implemented, 91 | * calls to purple_timeout_add_seconds() will be serviced by 92 | * #timeout_add. 93 | * 94 | * @see purple_timeout_add_seconds() 95 | * @since 2.1.0 96 | **/ 97 | guint timeout_add_seconds(guint interval, GSourceFunc function, gpointer data) { 98 | return timeout_add(interval*1000, function, data); 99 | } 100 | 101 | void on_timer_close_complete(uv_handle_t* handle) 102 | { 103 | free(handle->data); 104 | free(handle); 105 | } 106 | 107 | 108 | /** 109 | * Should remove a callback timer. Analogous to g_source_remove in glib. 110 | * @param handle an identifier for a timeout, as returned by 111 | * #timeout_add. 112 | * @return @c TRUE if the timeout identified by @a handle was 113 | * found and removed. 114 | * @see purple_timeout_remove 115 | */ 116 | gboolean timeout_remove(guint int_handle) { 117 | gpointer handle = GUINT_TO_POINTER(int_handle); 118 | g_return_val_if_fail(handle != NULL, false); 119 | s_evLoopTimer *timer = handle; 120 | uv_timer_stop(timer->handle); 121 | if (!uv_is_closing((uv_handle_t*)timer->handle)) { 122 | uv_close((uv_handle_t*)timer->handle, on_timer_close_complete); 123 | } 124 | return true; 125 | } 126 | 127 | /** 128 | * Handle input event from a poll. 129 | * 130 | * @param handle the libuv poll handle. 131 | * @param status the status of the poll handle. 0 for OK, negative for not-ok. 132 | * @param events the type of event raised (uv_poll_event), either 1 for read or 2 for write. 133 | */ 134 | void handle_input(uv_poll_t* handle, int status, int events) { 135 | if (status < 0) { 136 | g_warning("handle_input error status %i %s\n", status, uv_strerror(status)); 137 | // XXX: Do we need to do anything if the status is not ok? 138 | } else if (status > 0) { 139 | // Unexpected positive status 140 | g_warning("handle_input unexpected positive status %i\n", status); 141 | } 142 | int closedFD = -1; 143 | s_evLoopInput *input = handle->data; 144 | GList *elem; 145 | s_evLoopInputEvent *inputEvent; 146 | for(elem = input->events; elem; elem = elem->next) { 147 | inputEvent = elem->data; 148 | if (inputEvent->events & events) { 149 | inputEvent->func(inputEvent->user_data, inputEvent->parent->fd, events); 150 | } 151 | } 152 | } 153 | 154 | 155 | /** 156 | * Should add an input handler. Analogous to g_io_add_watch_full in 157 | * glib. 158 | * 159 | * @param fd a file descriptor to watch for events 160 | * @param cond a bitwise OR of events on @a fd for which @a func 161 | * should be called. 162 | * @param func a callback to fire whenever a relevant event on @a 163 | * fd occurs. 164 | * @param user_data arbitrary data to pass to @a fd. 165 | * @return an identifier for this input handler, which can be 166 | * passed to #input_remove. 167 | * 168 | * @see purple_input_add 169 | */ 170 | guint input_add(int fd, PurpleInputCondition cond, 171 | PurpleInputFunction func, gpointer user_data) { 172 | // Ensure we do not attempt to create a handle for invalid conditions. 173 | g_return_val_if_fail(cond == PURPLE_INPUT_READ || cond == PURPLE_INPUT_WRITE, GPOINTER_TO_UINT(NULL)); 174 | g_return_val_if_fail(fd > 0, GPOINTER_TO_UINT(NULL)); 175 | 176 | /** 177 | * There is some subtle logic to this function. LibUV can only handle 178 | * one poll handle per FD, but libpurple wants us to be able to support 179 | * multiple per FD. This means we need to manage multiple inputs for 180 | * a single handle ourselves. 181 | */ 182 | 183 | // Create a struct to hold THIS event/func pair 184 | s_evLoopInputEvent *input_event = g_malloc(sizeof(s_evLoopInputEvent)); 185 | input_event->events = cond; // Mapping to equivalent values in uv_poll_event. 186 | input_event->func = func; 187 | input_event->user_data = user_data; 188 | 189 | s_evLoopInput *input_handle = g_hash_table_lookup(evLoopState.inputs, &fd); 190 | if (input_handle == NULL) { 191 | input_handle = g_malloc(sizeof(s_evLoopInput)); 192 | input_handle->fd = fd; 193 | input_handle->handle = g_malloc(sizeof(uv_poll_t)); 194 | input_handle->cond = cond; 195 | input_handle->events = NULL; 196 | uv_handle_set_data((uv_handle_t*)input_handle->handle, input_handle); 197 | uv_poll_init(evLoopState.loop, input_handle->handle, fd); 198 | g_hash_table_insert(evLoopState.inputs, &input_handle->fd, input_handle); 199 | } else { 200 | // Nothing to do, except update the condition on the poll 201 | input_handle->cond |= cond; 202 | } 203 | // This will update the handle if the cond changed. 204 | uv_poll_start(input_handle->handle, input_handle->cond, handle_input); 205 | input_event->parent = input_handle; 206 | input_handle->events = g_list_append(input_handle->events, input_event); 207 | return GPOINTER_TO_UINT(input_event); 208 | } 209 | 210 | /** 211 | * Should remove an input handler. Analogous to g_source_remove in glib. 212 | * @param handle an identifier, as returned by #input_add. 213 | * @return @c TRUE if the input handler was found and removed. 214 | * @see purple_input_remove 215 | */ 216 | gboolean input_remove (guint int_handle) { 217 | gpointer handle = GUINT_TO_POINTER(int_handle); 218 | g_return_val_if_fail(handle != NULL, false); 219 | s_evLoopInputEvent *inputEvent = handle; 220 | s_evLoopInput *input = inputEvent->parent; 221 | if (g_list_find(input->events, inputEvent) == NULL) { 222 | return false; 223 | } 224 | input->events = g_list_remove(input->events, inputEvent); 225 | free(inputEvent); 226 | guint listeners = g_list_length(input->events); 227 | if (listeners > 0) { 228 | // TODO: We should change the flags for the poll handle here. 229 | return true; 230 | // Do not clean up the handle yet. 231 | } 232 | uv_poll_stop(input->handle); 233 | g_hash_table_remove(evLoopState.inputs, &input->fd); 234 | free(input->handle); 235 | free(input); 236 | return true; 237 | } 238 | 239 | static PurpleEventLoopUiOps glib_eventloops = 240 | { 241 | timeout_add, 242 | timeout_remove, 243 | input_add, 244 | input_remove, 245 | NULL,//input_get_error 246 | timeout_add_seconds,//timeout_add_seconds 247 | NULL, 248 | NULL, 249 | NULL, 250 | }; 251 | 252 | /** End of the eventloop functions. **/ 253 | PurpleEventLoopUiOps* eventLoop_get(napi_env* env) { 254 | if (evLoopState.loop == NULL){ 255 | // Initiate 256 | if (napi_get_uv_event_loop(*env, &evLoopState.loop) != napi_ok) { 257 | THROW(*env, NULL, "Could not get UV loop", NULL); 258 | } 259 | evLoopState.inputs = g_hash_table_new_full(g_int_hash, g_int_equal, NULL, NULL); 260 | } 261 | return &glib_eventloops; 262 | } 263 | 264 | 265 | void call_callback(uv_timer_t* handle) { 266 | s_evLoopTimer *timer = handle->data; 267 | purple_eventloop_set_ui_ops(&glib_eventloops); 268 | if (timer->handle == NULL) { 269 | return; 270 | } 271 | gboolean res = timer->function(timer->data); 272 | // If the function succeeds, continue 273 | if (!res && !uv_is_closing((uv_handle_t *)timer->handle)) { 274 | uv_close((uv_handle_t *)timer->handle, on_timer_close_complete); 275 | return; 276 | } 277 | uv_timer_again(handle); 278 | } 279 | 280 | 281 | /** 282 | * If implemented, should get the current error status for an input. 283 | * 284 | * Implementation of this UI op is optional. Implement it if the UI's 285 | * sockets or event loop needs to customize determination of socket 286 | * error status. If unimplemented, getsockopt(2) will be used 287 | * instead. 288 | * 289 | * @see purple_input_get_error 290 | */ 291 | // int input_get_error(int fd, int *error) { 292 | 293 | // } 294 | -------------------------------------------------------------------------------- /src/eventloop.h: -------------------------------------------------------------------------------- 1 | #ifndef EVENTLOOP_H_INCLUDED 2 | #define EVENTLOOP_H_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | PurpleEventLoopUiOps *eventLoop_get(napi_env* env); 10 | guint timeout_add (guint interval, GSourceFunc function, gpointer data); 11 | #endif 12 | -------------------------------------------------------------------------------- /src/helper.c: -------------------------------------------------------------------------------- 1 | #include "helper.h" 2 | 3 | bool getValueFromObject(napi_env env, napi_value object, char* propStr, napi_valuetype *type, napi_value *value); 4 | 5 | typedef struct { 6 | int32_t debugEnabled; 7 | napi_value eventFunc; 8 | char* userDir; 9 | char* pluginDir; 10 | } s_setupPurple; 11 | 12 | void getSetupPurpleStruct(napi_env env, napi_callback_info info, s_setupPurple* setupOpts) { 13 | size_t argc = 1; 14 | napi_value opts; 15 | napi_valuetype type; 16 | napi_value value; 17 | napi_get_cb_info(env, info, &argc, &opts, NULL, NULL); 18 | if (argc == 0) { 19 | THROW(env, NULL, "setupPurple takes a options object"); 20 | } 21 | 22 | /* debugEnabled */ 23 | if (getValueFromObject(env, opts, "debugEnabled", &type, &value)) { 24 | napi_get_value_int32(env, value, &setupOpts->debugEnabled); 25 | } 26 | 27 | if (napi_ok != napi_get_named_property(env, opts, "eventFunc", &setupOpts->eventFunc)) { 28 | THROW(env, NULL, "setupPurple expects eventFunc to be defined"); 29 | } 30 | 31 | /* userDir */ 32 | if (getValueFromObject(env, opts, "userDir", &type, &value) && type == napi_string) { 33 | setupOpts->userDir = napi_help_strfromval(env, value); 34 | } else { 35 | setupOpts->userDir = NULL; 36 | } 37 | 38 | /* pluginDir */ 39 | if (getValueFromObject(env, opts, "pluginDir", &type, &value) && type == napi_string) { 40 | setupOpts->pluginDir = napi_help_strfromval(env, value); 41 | } else { 42 | setupOpts->pluginDir = NULL; 43 | } 44 | } 45 | 46 | napi_value pollEvents(napi_env env, napi_callback_info info) { 47 | napi_value eventArray; 48 | napi_value evtObj; 49 | napi_create_array(env, &eventArray); 50 | int i = 0; 51 | s_signalEventData *evtData; 52 | 53 | GSList* eventQueue = signalling_pop(); 54 | 55 | while(eventQueue != NULL) { 56 | evtData = (s_signalEventData*)eventQueue->data; 57 | evtObj = getJsObjectForSignalEvent(env, evtData); 58 | napi_set_element(env, eventArray, i, evtObj); 59 | if (evtData->freeMe) { 60 | free(evtData->data); 61 | } 62 | free(eventQueue->data); 63 | eventQueue = signalling_pop(); 64 | i++; 65 | } 66 | return eventArray; 67 | } 68 | 69 | void handlePurpleSignalCb(gpointer signalData, gpointer data) { 70 | s_signalCbData cbData = *(s_signalCbData*)data; 71 | s_signalEventData *ev = malloc(sizeof(s_signalEventData)); 72 | ev->signal = cbData.signal; 73 | ev->data = signalData; 74 | // Don't free this, it's not ours. 75 | ev->freeMe = false; 76 | signalling_push(ev); 77 | } 78 | 79 | void wirePurpleSignalsIntoNode(napi_env env, napi_value eventFunc) { 80 | static int handle; 81 | s_signalCbData *cbData; 82 | void *conn_handle = purple_connections_get_handle(); 83 | void *conv_handle = purple_conversations_get_handle(); 84 | void *accounts_handle = purple_accounts_get_handle(); 85 | 86 | cbData = malloc(sizeof(s_signalCbData)); 87 | cbData->signal = "signing-on"; 88 | purple_signal_connect(conn_handle, "signing-on", &handle, 89 | PURPLE_CALLBACK(handlePurpleSignalCb), cbData); 90 | 91 | cbData = malloc(sizeof(s_signalCbData)); 92 | cbData->signal = "account-signed-on"; 93 | purple_signal_connect(accounts_handle, "account-signed-on", &handle, 94 | PURPLE_CALLBACK(handlePurpleSignalCb), cbData); 95 | 96 | cbData = malloc(sizeof(s_signalCbData)); 97 | cbData->signal = "account-signed-off"; 98 | purple_signal_connect(accounts_handle, "account-signed-off", &handle, 99 | PURPLE_CALLBACK(handlePurpleSignalCb), cbData); 100 | 101 | cbData = malloc(sizeof(s_signalCbData)); 102 | cbData->signal = "account-disabled"; 103 | purple_signal_connect(accounts_handle, "account-disabled", &handle, 104 | PURPLE_CALLBACK(handlePurpleSignalCb), cbData); 105 | 106 | cbData = malloc(sizeof(s_signalCbData)); 107 | cbData->signal = "account-enabled"; 108 | purple_signal_connect_vargs(accounts_handle, "account-enabled", &handle, 109 | PURPLE_CALLBACK(handlePurpleSignalCb), cbData); 110 | 111 | cbData = malloc(sizeof(s_signalCbData)); 112 | cbData->signal = "account-connecting"; 113 | purple_signal_connect(accounts_handle, "account-connecting", &handle, 114 | PURPLE_CALLBACK(handlePurpleSignalCb), cbData); 115 | 116 | cbData = malloc(sizeof(s_signalCbData)); 117 | cbData->signal = "account-connection-error"; 118 | purple_signal_connect(accounts_handle, "account-connection-error", &handle, 119 | PURPLE_CALLBACK(handleAccountConnectionError), cbData); 120 | 121 | cbData = malloc(sizeof(s_signalCbData)); 122 | cbData->signal = "received-im-msg"; 123 | purple_signal_connect(conv_handle, "received-im-msg", &handle, 124 | PURPLE_CALLBACK(handleReceivedMessage), cbData); 125 | 126 | cbData = malloc(sizeof(s_signalCbData)); 127 | cbData->signal = "received-chat-msg"; 128 | purple_signal_connect(conv_handle, "received-chat-msg", &handle, 129 | PURPLE_CALLBACK(handleReceivedMessage), cbData); 130 | 131 | cbData->signal = "buddy-typing"; 132 | purple_signal_connect(conv_handle, "buddy-typing", &handle, 133 | PURPLE_CALLBACK(handleBuddyTyping), cbData); 134 | 135 | cbData->signal = "buddy-typing-stopped"; 136 | purple_signal_connect(conv_handle, "buddy-typing-stopped", &handle, 137 | PURPLE_CALLBACK(handleBuddyTypingStopped), cbData); 138 | 139 | 140 | purple_signal_connect(conv_handle, "chat-joined", &handle, 141 | PURPLE_CALLBACK(handleJoined), NULL); 142 | 143 | purple_signal_connect(conv_handle, "chat-invited", &handle, 144 | PURPLE_CALLBACK(handleInvited), NULL); 145 | } 146 | 147 | void _accounts_restore_current_statuses() 148 | { 149 | GList *l; 150 | PurpleAccount *account; 151 | 152 | /* If we're not connected to the Internet right now, we bail on this */ 153 | /*if (!purple_network_is_available()) 154 | { 155 | purple_debug_warning("account", "Network not connected; skipping reconnect\n"); 156 | return; 157 | }*/ 158 | unsigned timeout = 0; 159 | for (l = purple_accounts_get_all(); l != NULL; l = l->next) 160 | { 161 | account = (PurpleAccount *)l->data; 162 | if (purple_account_get_enabled(account, purple_core_get_ui()) && 163 | (purple_presence_is_online(account->presence))) 164 | { 165 | timeout_add(timeout, G_SOURCE_FUNC(purple_account_connect), account); 166 | timeout += 100; 167 | } 168 | } 169 | } 170 | 171 | napi_value setupPurple(napi_env env, napi_callback_info info) { 172 | napi_value n_undef; 173 | napi_get_undefined(env, &n_undef); 174 | 175 | s_setupPurple opts; 176 | PurpleConversationUiOps uiops = { 177 | NULL, /* create_conversation */ 178 | NULL, /* destroy_conversation */ 179 | NULL, /* write_chat */ 180 | NULL, /* write_im */ 181 | NULL, /* write_conv */ 182 | NULL, /* chat_add_users */ 183 | NULL, /* chat_rename_user */ 184 | NULL, /* chat_remove_users */ 185 | NULL, /* chat_update_user */ 186 | NULL, /* present */ 187 | NULL, /* has_focus */ 188 | NULL, /* custom_smiley_add */ 189 | NULL, /* custom_smiley_write */ 190 | NULL, /* custom_smiley_close */ 191 | NULL, /* send_confirm */ 192 | NULL, 193 | NULL, 194 | NULL, 195 | NULL 196 | }; 197 | PurpleNotifyUiOps *notifyopts = malloc(sizeof(PurpleNotifyUiOps)); 198 | notifyopts->notify_userinfo = handleUserInfo; 199 | purple_notify_set_ui_ops(notifyopts); 200 | PurpleEventLoopUiOps *evLoopOps = eventLoop_get(&env); 201 | if (evLoopOps == NULL) { 202 | return; 203 | } 204 | purple_eventloop_set_ui_ops(evLoopOps); 205 | 206 | getSetupPurpleStruct(env, info, &opts); 207 | purple_debug_set_enabled(opts.debugEnabled); 208 | if (opts.userDir != NULL) { 209 | // Purple copies these strings 210 | purple_util_set_user_dir(opts.userDir); 211 | free(opts.userDir); 212 | } 213 | 214 | if (opts.pluginDir != NULL) { 215 | // Purple copies these strings 216 | purple_plugins_add_search_path(opts.pluginDir); 217 | free(opts.pluginDir); 218 | } 219 | 220 | purple_prefs_load(); 221 | purple_set_blist(purple_blist_new()); 222 | purple_core_init(STR_PURPLE_UI); 223 | // To restore all the accounts. 224 | _accounts_restore_current_statuses(); 225 | // To get our buddies :3 226 | purple_blist_load(); 227 | wirePurpleSignalsIntoNode(env, opts.eventFunc); 228 | return n_undef; 229 | } 230 | 231 | /* N-API helpers */ 232 | 233 | bool getValueFromObject(napi_env env, napi_value object, char* propStr, napi_valuetype *type, napi_value *value) { 234 | napi_status status; 235 | napi_value propName; 236 | bool hasProperty; 237 | status = napi_create_string_utf8(env, propStr, NAPI_AUTO_LENGTH, &propName); 238 | if (status != napi_ok) { 239 | THROW(env, NULL, "Could not get value from object: Could not create string", false); 240 | } 241 | status = napi_has_property(env, object, propName, &hasProperty); 242 | if (status != napi_ok) { 243 | THROW(env, NULL, "Could not get value from object: Could not get property", false); 244 | } 245 | if (!hasProperty) { 246 | return false; 247 | } 248 | status = napi_get_property(env, object, propName, value); 249 | if (status != napi_ok) { 250 | THROW(env, NULL, "Could not get value from object: Could not get property", false); 251 | } 252 | status = napi_typeof(env, *value, type); 253 | if (status != napi_ok) { 254 | THROW(env, NULL, "Could not get value from object: Could not get type", false); 255 | } 256 | return true; 257 | } 258 | -------------------------------------------------------------------------------- /src/helper.h: -------------------------------------------------------------------------------- 1 | #ifndef HELPER_H_INCLUDED 2 | #define HELPER_H_INCLUDED 3 | 4 | #define STR_PURPLE_UI "matrix-bridge" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "eventloop.h" 14 | #include "signalling.h" 15 | 16 | napi_value setupPurple(napi_env env, napi_callback_info info); 17 | napi_value pollEvents(napi_env env, napi_callback_info info); 18 | 19 | #define THROW(env, code, message, ...) do { napi_throw_error(env, code, message); return __VA_ARGS__; } while (0) 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/messaging.c: -------------------------------------------------------------------------------- 1 | #include "messaging.h" 2 | #include "helper.h" 3 | 4 | napi_value messaging_sendIM(napi_env env, napi_callback_info info) { 5 | PurpleAccount* account; 6 | size_t argc = 3; 7 | napi_value opts[3]; 8 | char* name; 9 | char* body; 10 | 11 | napi_get_cb_info(env, info, &argc, opts, NULL, NULL); 12 | if (argc < 3) { 13 | THROW(env, NULL, "sendIM takes three arguments", NULL); 14 | } 15 | 16 | // Get the account 17 | napi_get_value_external(env, opts[0], (void*)&account); 18 | 19 | if (account == NULL) { 20 | THROW(env, NULL, "account is null", NULL); 21 | } 22 | 23 | name = napi_help_strfromval(env, opts[1]); 24 | 25 | const PurpleConversation* conv = purple_find_conversation_with_account( 26 | PURPLE_CONV_TYPE_IM, 27 | name, 28 | account 29 | ); 30 | 31 | if (conv == NULL) { 32 | // Create one. 33 | conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, name); 34 | } 35 | // Get the IM 36 | PurpleConvIm* convIm = purple_conversation_get_im_data(conv); 37 | 38 | body = napi_help_strfromval(env, opts[2]); 39 | 40 | purple_conv_im_send(convIm, body); 41 | free(name); 42 | free(body); 43 | 44 | return NULL; 45 | } 46 | 47 | napi_value messaging_sendChat(napi_env env, napi_callback_info info) { 48 | PurpleAccount* account; 49 | size_t argc = 3; 50 | napi_value opts[3]; 51 | char* name; 52 | char* body; 53 | 54 | napi_get_cb_info(env, info, &argc, opts, NULL, NULL); 55 | if (argc < 3) { 56 | THROW(env, NULL, "sendIM takes three arguments", NULL); 57 | } 58 | 59 | // Get the account 60 | napi_get_value_external(env, opts[0], (void*)&account); 61 | 62 | if (account == NULL) { 63 | THROW(env, NULL, "account is null", NULL); 64 | } 65 | 66 | name = napi_help_strfromval(env, opts[1]); 67 | 68 | const PurpleConversation* conv = purple_find_conversation_with_account( 69 | PURPLE_CONV_TYPE_CHAT, 70 | name, 71 | account 72 | ); 73 | 74 | if (conv == NULL) { 75 | THROW(env, NULL, "conversation not found", NULL); 76 | } 77 | // Get the chat 78 | PurpleConvChat* convChat = purple_conversation_get_chat_data(conv); 79 | 80 | body = napi_help_strfromval(env, opts[2]); 81 | 82 | purple_conv_chat_send(convChat, body); 83 | free(name); 84 | free(body); 85 | 86 | return NULL; 87 | } 88 | 89 | napi_value messaging_chatParams(napi_env env, napi_callback_info info) { 90 | PurplePlugin* plugin; 91 | PurpleAccount* account; 92 | PurpleConnection* connection; 93 | size_t argc = 2; 94 | napi_value opts[2]; 95 | char* protocolId; 96 | napi_value property_array; 97 | struct proto_chat_entry *pce; 98 | 99 | napi_get_cb_info(env, info, &argc, opts, NULL, NULL); 100 | if (argc < 2) { 101 | THROW(env, NULL, "chatParams takes two arguments", NULL); 102 | } 103 | 104 | // Get the account 105 | napi_get_value_external(env, opts[0], (void*)&account); 106 | 107 | connection = purple_account_get_connection(account); 108 | protocolId = napi_help_strfromval(env, opts[1]); 109 | 110 | plugin = purple_find_prpl(protocolId); 111 | free(protocolId); 112 | 113 | napi_create_array(env, &property_array); 114 | GList* parts = PURPLE_PLUGIN_PROTOCOL_INFO(plugin)->chat_info(connection); 115 | GList* l; 116 | uint32_t i = 0; 117 | for (l = parts; l != NULL; l = l->next) 118 | { 119 | pce = l->data; 120 | napi_value obj; 121 | napi_value value; 122 | napi_create_object(env, &obj); 123 | 124 | napi_create_string_utf8(env, pce->identifier, NAPI_AUTO_LENGTH, &value); 125 | napi_set_named_property(env, obj, "identifier", value); 126 | 127 | napi_create_string_utf8(env, pce->label, NAPI_AUTO_LENGTH, &value); 128 | napi_set_named_property(env, obj, "label", value); 129 | 130 | napi_get_boolean(env, pce->required, &value); 131 | napi_set_named_property(env, obj, "required", value); 132 | 133 | napi_set_element(env, property_array, i, obj); 134 | i++; 135 | } 136 | 137 | return property_array; 138 | } 139 | 140 | napi_value messaging_joinChat(napi_env env, napi_callback_info info) { 141 | PurpleAccount* account; 142 | PurpleConnection* conn; 143 | size_t argc = 2; 144 | napi_value opts[2]; 145 | 146 | napi_get_cb_info(env, info, &argc, opts, NULL, NULL); 147 | if (argc < 2) { 148 | THROW(env, NULL, "joinChat takes two arguments", NULL); 149 | } 150 | 151 | // Get the account 152 | napi_get_value_external(env, opts[0], (void*)&account); 153 | conn = purple_account_get_connection(account); 154 | 155 | GHashTable* components = g_hash_table_new_full( 156 | g_str_hash, 157 | g_str_equal, 158 | free, 159 | free 160 | ); 161 | napi_value nComponentNames; 162 | napi_value jkey; 163 | napi_value jvalue; 164 | napi_valuetype type; 165 | char* key; 166 | char* value; 167 | u_int32_t length; 168 | napi_get_property_names(env, opts[1], &nComponentNames); 169 | napi_get_array_length(env, nComponentNames, &length); 170 | for(u_int32_t i = 0; i < length;i++) { 171 | napi_get_element(env, nComponentNames, i, &jkey); 172 | napi_get_property(env, opts[1], jkey, &jvalue); 173 | napi_typeof(env, jvalue, &type); 174 | if (type == napi_string) { 175 | // We only support strings. 176 | key = napi_help_strfromval(env, jkey); 177 | value = napi_help_strfromval(env, jvalue); 178 | g_hash_table_insert(components, key, value); 179 | } 180 | } 181 | serv_join_chat(conn, components); 182 | g_hash_table_remove_all(components); 183 | 184 | return NULL; 185 | } 186 | 187 | napi_value messaging_rejectChat(napi_env env, napi_callback_info info) { 188 | return NULL; 189 | } 190 | 191 | napi_value messaging_getNickForChat(napi_env env, napi_callback_info info) { 192 | // purple_conv_chat_cb_find 193 | PurpleConversationType type; 194 | PurpleConversation* conv; 195 | napi_value res; 196 | size_t argc = 1; 197 | napi_value opts[1]; 198 | 199 | napi_get_cb_info(env, info, &argc, opts, NULL, NULL); 200 | if (argc < 1) { 201 | THROW(env, NULL, "getNickForChat takes one argument", NULL); 202 | } 203 | napi_get_value_external(env, opts[0], (void*)&conv); 204 | type = purple_conversation_get_type(conv); 205 | if (type == PURPLE_CONV_TYPE_CHAT) { 206 | PurpleConvChat* chat = purple_conversation_get_chat_data(conv); 207 | const char* nick = purple_conv_chat_get_nick(chat); 208 | napi_create_string_utf8(env, nick, NAPI_AUTO_LENGTH, &res); 209 | } else { 210 | THROW(env, NULL, "conversation was not PURPLE_CONV_TYPE_CHAT", NULL); 211 | } 212 | return res; 213 | } 214 | 215 | napi_value messaging_findConversation(napi_env env, napi_callback_info info) { 216 | PurpleAccount* account; 217 | size_t argc = 2; 218 | napi_value opts[2]; 219 | char* name; 220 | 221 | napi_get_cb_info(env, info, &argc, opts, NULL, NULL); 222 | if (argc < 2) { 223 | THROW(env, NULL, "findConversation takes two arguments", NULL); 224 | } 225 | 226 | // Get the account 227 | napi_get_value_external(env, opts[0], (void*)&account); 228 | 229 | if (account == NULL) { 230 | THROW(env, NULL, "account is null", NULL); 231 | } 232 | 233 | name = napi_help_strfromval(env, opts[1]); 234 | 235 | PurpleConversation* conv = purple_find_conversation_with_account( 236 | PURPLE_CONV_TYPE_ANY, 237 | name, 238 | account 239 | ); 240 | 241 | free(name); 242 | if (conv == NULL) { 243 | THROW(env, NULL, "conversation not found", NULL); 244 | } 245 | 246 | return nprpl_conv_create(env, conv); 247 | } 248 | 249 | napi_value messaging_set_im_typing_state(napi_env env, napi_callback info) { 250 | PurpleAccount* account; 251 | size_t argc = 3; 252 | napi_value opts[3]; 253 | char* name; 254 | 255 | napi_get_cb_info(env, info, &argc, opts, NULL, NULL); 256 | if (argc < 3) { 257 | THROW(env, NULL, "setIMTypingState takes three arguments", NULL); 258 | } 259 | 260 | // Get the account 261 | napi_get_value_external(env, opts[0], (void*)&account); 262 | 263 | if (account == NULL) { 264 | THROW(env, NULL, "account is null", NULL); 265 | } 266 | 267 | name = napi_help_strfromval(env, opts[1]); 268 | 269 | const PurpleConversation* conv = purple_find_conversation_with_account( 270 | PURPLE_CONV_TYPE_IM, 271 | name, 272 | account 273 | ); 274 | 275 | if (conv == NULL) { 276 | THROW(env, NULL, "Cannot set typing notification as conv doesn't exist", NULL); 277 | } 278 | 279 | int32_t state; 280 | 281 | if (napi_get_value_int32(env, opts[2], &state) != napi_ok) { 282 | THROW(env, NULL, "could not determine typing state from arg", NULL); 283 | } 284 | g_return_if_fail(state >= 0 && state <= 2); 285 | 286 | serv_send_typing( 287 | purple_conversation_get_gc(conv), 288 | name, 289 | state 290 | ); 291 | free(name); 292 | 293 | return NULL; 294 | 295 | } 296 | 297 | void messaging_bind_node(napi_env env,napi_value root) { 298 | napi_value namespace; 299 | napi_value _func; 300 | napi_create_object(env, &namespace); 301 | 302 | napi_create_function(env, NULL, 0, messaging_sendIM, NULL, &_func); 303 | napi_set_named_property(env, namespace, "sendIM", _func); 304 | 305 | napi_create_function(env, NULL, 0, messaging_set_im_typing_state, NULL, &_func); 306 | napi_set_named_property(env, namespace, "setIMTypingState", _func); 307 | 308 | napi_create_function(env, NULL, 0, messaging_sendChat, NULL, &_func); 309 | napi_set_named_property(env, namespace, "sendChat", _func); 310 | 311 | napi_create_function(env, NULL, 0, messaging_chatParams, NULL, &_func); 312 | napi_set_named_property(env, namespace, "chatParams", _func); 313 | 314 | napi_create_function(env, NULL, 0, messaging_joinChat, NULL, &_func); 315 | napi_set_named_property(env, namespace, "joinChat", _func); 316 | 317 | napi_create_function(env, NULL, 0, messaging_rejectChat, NULL, &_func); 318 | napi_set_named_property(env, namespace, "rejectChat", _func); 319 | 320 | napi_create_function(env, NULL, 0, messaging_getNickForChat, NULL, &_func); 321 | napi_set_named_property(env, namespace, "getNickForChat", _func); 322 | 323 | napi_create_function(env, NULL, 0, messaging_findConversation, NULL, &_func); 324 | napi_set_named_property(env, namespace, "findConversation", _func); 325 | 326 | napi_set_named_property(env, root, "messaging", namespace); 327 | } 328 | -------------------------------------------------------------------------------- /src/messaging.h: -------------------------------------------------------------------------------- 1 | #ifndef MESSAGING_H_INCLUDED 2 | #define MESSAGING_H_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "napi_helpers.h" 11 | 12 | void messaging_bind_node(napi_env env,napi_value root); 13 | #endif 14 | -------------------------------------------------------------------------------- /src/module.c: -------------------------------------------------------------------------------- 1 | #define NAPI_VERSION 3 2 | #define NAPI_EXPERIMENTAL 3 | #include 4 | #include 5 | 6 | #include "bindings/b_core.h" 7 | #include "bindings/b_plugins.h" 8 | #include "bindings/b_accounts.h" 9 | #include "bindings/b_buddy.h" 10 | #include "bindings/b_notify.h" 11 | #include "helper.h" 12 | #include "messaging.h" 13 | 14 | 15 | napi_value Init(napi_env env, napi_value exports) { 16 | // Future binding should expose a XXX_bind_node(env,exports) function 17 | // and apply their exported functions to that rather than use a header. 18 | // The stuff below is mostly wrong. 19 | napi_value _fn_purple_core_get_version; 20 | napi_value _fn_purple_core_init; 21 | napi_value _fn_purple_core_quit; 22 | napi_value _func; 23 | 24 | /* Create core */ 25 | napi_value ns_core; 26 | napi_create_object(env, &ns_core); 27 | 28 | napi_create_function(env, NULL, 0, _purple_core_get_version, NULL, &_fn_purple_core_get_version); 29 | napi_set_named_property(env, ns_core, "get_version", _fn_purple_core_get_version); 30 | 31 | napi_create_function(env, NULL, 0, _purple_core_init, NULL, &_fn_purple_core_init); 32 | napi_set_named_property(env, ns_core, "init", _fn_purple_core_init); 33 | 34 | napi_create_function(env, NULL, 0, _purple_core_quit, NULL, &_fn_purple_core_quit); 35 | napi_set_named_property(env, ns_core, "quit", _fn_purple_core_quit); 36 | 37 | napi_set_named_property(env, exports, "core", ns_core); 38 | 39 | /* Create debug */ 40 | napi_value ns_debug; 41 | napi_create_object(env, &ns_debug); 42 | 43 | napi_set_named_property(env, exports, "debug", ns_debug); 44 | 45 | /* Create helper */ 46 | napi_value ns_helper; 47 | napi_create_object(env, &ns_helper); 48 | 49 | napi_create_function(env, NULL, 0, setupPurple, NULL, &_func); 50 | napi_set_named_property(env, ns_helper, "setupPurple", _func); 51 | 52 | napi_create_function(env, NULL, 0, pollEvents, NULL, &_func); 53 | napi_set_named_property(env, ns_helper, "pollEvents", _func); 54 | 55 | napi_set_named_property(env, exports, "helper", ns_helper); 56 | 57 | /* Create plugins */ 58 | napi_value ns_plugins; 59 | napi_create_object(env, &ns_plugins); 60 | 61 | napi_create_function(env, NULL, 0, _purple_plugins_get_protocols, NULL, &_func); 62 | napi_set_named_property(env, ns_plugins, "get_protocols", _func); 63 | 64 | napi_set_named_property(env, exports, "plugins", ns_plugins); 65 | 66 | 67 | // This is the right way to do it :) 68 | accounts_bind_node(env, exports); 69 | messaging_bind_node(env, exports); 70 | buddy_bind_node(env, exports); 71 | notify_bind_node(env, exports); 72 | 73 | return exports; 74 | } 75 | 76 | 77 | NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) 78 | -------------------------------------------------------------------------------- /src/napi_helpers.c: -------------------------------------------------------------------------------- 1 | #include "napi_helpers.h" 2 | char* napi_help_strfromval(napi_env env, napi_value opt) { 3 | size_t length = 0; 4 | char* buffer; 5 | // TODO: This NEEDS to check the status 6 | napi_get_value_string_utf8(env, opt, NULL, 0, &length); 7 | length++; //Null terminator 8 | buffer = malloc(sizeof(char) * length); 9 | napi_get_value_string_utf8(env, opt, buffer, length, NULL); 10 | return buffer; 11 | } 12 | 13 | napi_value nprpl_conv_create(napi_env env, PurpleConversation *conv) { 14 | napi_value obj; 15 | napi_value value; 16 | napi_create_object(env, &obj); 17 | purple_conversation_get_name(conv); 18 | 19 | const char *sval = purple_conversation_get_name(conv); 20 | 21 | napi_create_string_utf8(env, sval, NAPI_AUTO_LENGTH, &value); 22 | napi_set_named_property(env, obj, "name", value); 23 | 24 | /* handle */ 25 | napi_create_external(env, conv, NULL, NULL, &value); 26 | napi_set_named_property(env, obj, "handle", value); 27 | 28 | return obj; 29 | } 30 | -------------------------------------------------------------------------------- /src/napi_helpers.h: -------------------------------------------------------------------------------- 1 | #ifndef NAPI_HELPERS_H_INCLUDED 2 | #define NAPI_HELPERS_H_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | char* napi_help_strfromval(napi_env env, napi_value opt); 9 | napi_value nprpl_conv_create(napi_env env, PurpleConversation *conv); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /src/signalling.c: -------------------------------------------------------------------------------- 1 | #include "signalling.h" 2 | 3 | static GSList *eventQueue = NULL; // s_signalEventData 4 | 5 | typedef struct { 6 | PurpleAccount *account; 7 | char *sender; 8 | char *buffer; 9 | PurpleConversation *conv; 10 | PurpleMessageFlags flags; 11 | void *data; 12 | } s_EventDataImMessage; 13 | 14 | typedef struct { 15 | PurpleAccount *account; 16 | char *description; 17 | PurpleConnectionError type; 18 | } s_EventDataConnectionError; 19 | 20 | typedef struct { 21 | PurpleAccount *account; 22 | char *sender; 23 | char *roomName; 24 | char *message; 25 | GHashTable *inviteProps; 26 | } s_EventDataInvited; 27 | 28 | typedef struct { 29 | PurpleAccount *account; 30 | char* who; 31 | GList* items; 32 | } e_UserInfoResponse; 33 | 34 | typedef struct { 35 | char* label; 36 | char* value; 37 | } e_UserInfoResponseItem; 38 | 39 | typedef struct { 40 | PurpleAccount *account; 41 | char* sender; 42 | bool typing; 43 | } e_UserTyping; 44 | 45 | napi_value getJsObjectForSignalEvent(napi_env env, s_signalEventData *eventData) { 46 | napi_value evtObj; 47 | napi_value value; 48 | napi_create_object(env, &evtObj); 49 | napi_create_string_utf8(env, eventData->signal, NAPI_AUTO_LENGTH, &value); 50 | napi_set_named_property(env, evtObj, "eventName", value); 51 | /* This is where things get a bit messy, we want to extract information about each event */ 52 | if ( 53 | strcmp(eventData->signal, "account-signed-on") == 0 || 54 | strcmp(eventData->signal, "account-signed-off") == 0 || 55 | strcmp(eventData->signal, "account-added") == 0 || 56 | strcmp(eventData->signal, "account-removed") == 0 || 57 | strcmp(eventData->signal, "account-connecting") == 0 || 58 | strcmp(eventData->signal, "account-created") == 0 || 59 | strcmp(eventData->signal, "account-destroying") == 0 60 | ) { 61 | PurpleAccount* prplAcct = (PurpleAccount*)eventData->data; 62 | napi_value acct = nprpl_account_create(env, prplAcct); 63 | napi_set_named_property(env, evtObj, "account", acct); 64 | } 65 | 66 | if (strcmp(eventData->signal, "account-connection-error") == 0) { 67 | s_EventDataConnectionError msgData = *(s_EventDataConnectionError*)eventData->data; 68 | napi_value acct = nprpl_account_create(env, msgData.account); 69 | napi_set_named_property(env, evtObj, "account", acct); 70 | 71 | napi_create_string_utf8(env, msgData.description, NAPI_AUTO_LENGTH, &value); 72 | napi_set_named_property(env, evtObj, "description", value); 73 | 74 | napi_create_uint32(env, msgData.type,&value); 75 | napi_set_named_property(env, evtObj, "type", value); 76 | } 77 | 78 | if (strcmp(eventData->signal, "received-im-msg") == 0 || 79 | strcmp(eventData->signal, "received-chat-msg") == 0) { 80 | s_EventDataImMessage msgData = *(s_EventDataImMessage*)eventData->data; 81 | napi_value acct = nprpl_account_create(env, msgData.account); 82 | napi_set_named_property(env, evtObj, "account", acct); 83 | 84 | napi_create_string_utf8(env, msgData.buffer, NAPI_AUTO_LENGTH, &value); 85 | napi_set_named_property(env, evtObj, "message", value); 86 | g_free(msgData.buffer); 87 | 88 | napi_create_string_utf8(env, msgData.sender, NAPI_AUTO_LENGTH, &value); 89 | napi_set_named_property(env, evtObj, "sender", value); 90 | g_free(msgData.sender); 91 | if (msgData.conv != NULL) { 92 | value = nprpl_conv_create(env, msgData.conv); 93 | napi_set_named_property(env, evtObj, "conv", value); 94 | } 95 | } 96 | 97 | if(strcmp(eventData->signal, "user-info-response") == 0) { 98 | e_UserInfoResponse msgData = *(e_UserInfoResponse*)eventData->data; 99 | napi_value jkey, jvalue; 100 | GList* l; 101 | for (l = msgData.items; l != NULL; l = l->next) { 102 | e_UserInfoResponseItem item = *(e_UserInfoResponseItem*)l->data; 103 | napi_create_string_utf8(env, item.label, NAPI_AUTO_LENGTH, &jkey); 104 | if (item.value != NULL) { 105 | napi_create_string_utf8(env, item.value, NAPI_AUTO_LENGTH, &jvalue); 106 | } else { 107 | napi_get_undefined(env, &jvalue); 108 | } 109 | napi_set_property(env, evtObj, jkey, jvalue); 110 | } 111 | g_list_free_full(msgData.items, free); 112 | 113 | napi_value acct = nprpl_account_create(env, msgData.account); 114 | napi_set_named_property(env, evtObj, "account", acct); 115 | 116 | napi_create_string_utf8(env, msgData.who, NAPI_AUTO_LENGTH, &value); 117 | napi_set_named_property(env, evtObj, "who", value); 118 | } 119 | 120 | if (strcmp(eventData->signal, "chat-joined") == 0) { 121 | s_EventDataImMessage msgData = *(s_EventDataImMessage*)eventData->data; 122 | napi_value acct = nprpl_account_create(env, msgData.account); 123 | napi_set_named_property(env, evtObj, "account", acct); 124 | 125 | value = nprpl_conv_create(env, msgData.conv); 126 | napi_set_named_property(env, evtObj, "conv", value); 127 | } 128 | 129 | if (strcmp(eventData->signal, "chat-invite") == 0) { 130 | s_EventDataInvited msgData = *(s_EventDataInvited*)eventData->data; 131 | napi_value acct = nprpl_account_create(env, msgData.account); 132 | napi_set_named_property(env, evtObj, "account", acct); 133 | if (msgData.message != NULL) { 134 | napi_create_string_utf8(env, msgData.message, NAPI_AUTO_LENGTH, &value); 135 | napi_set_named_property(env, evtObj, "message", value); 136 | } 137 | 138 | if (msgData.roomName != NULL) { 139 | napi_create_string_utf8(env, msgData.roomName, NAPI_AUTO_LENGTH, &value); 140 | napi_set_named_property(env, evtObj, "room_name", value); 141 | } 142 | 143 | if(msgData.sender != NULL) { 144 | napi_create_string_utf8(env, msgData.sender, NAPI_AUTO_LENGTH, &value); 145 | napi_set_named_property(env, evtObj, "sender", value); 146 | } 147 | 148 | napi_create_object(env, &value); 149 | GHashTableIter iter; 150 | gpointer key, val; 151 | napi_value jkey, jvalue; 152 | 153 | g_hash_table_iter_init (&iter, msgData.inviteProps); 154 | while (g_hash_table_iter_next (&iter, &key, &val)) 155 | { 156 | char* skey = (char*)key; 157 | char* sval = (char*)val; 158 | napi_create_string_utf8(env, skey, NAPI_AUTO_LENGTH, &jkey); 159 | if (sval != NULL) { 160 | napi_create_string_utf8(env, sval, NAPI_AUTO_LENGTH, &jvalue); 161 | } else { 162 | napi_get_undefined(env, &jvalue); 163 | } 164 | napi_set_property(env, value, jkey, jvalue); 165 | 166 | } 167 | napi_set_named_property(env, evtObj, "join_properties", value); 168 | } 169 | 170 | if (strcmp(eventData->signal, "im-typing") == 0) { 171 | e_UserTyping typingData = *(e_UserTyping*)eventData->data; 172 | napi_value acct = nprpl_account_create(env, typingData.account); 173 | napi_set_named_property(env, evtObj, "account", acct); 174 | 175 | napi_create_string_utf8(env, typingData.sender, NAPI_AUTO_LENGTH, &value); 176 | napi_set_named_property(env, evtObj, "sender", value); 177 | 178 | napi_get_boolean(env, typingData.typing, &value); 179 | napi_set_named_property(env, evtObj, "typing", value); 180 | } 181 | return evtObj; 182 | } 183 | 184 | void signalling_push(s_signalEventData *eventData) { 185 | eventQueue = g_slist_append(eventQueue, eventData); 186 | } 187 | 188 | GSList *signalling_pop() { 189 | GSList* item = eventQueue; 190 | if (item != NULL) { 191 | eventQueue = g_slist_remove_link(eventQueue, item); 192 | } 193 | return item; 194 | } 195 | 196 | /* Handle specific callbacks below */ 197 | 198 | void handleReceivedMessage(PurpleAccount *account, char *sender, char *buffer, PurpleConversation *conv, 199 | PurpleMessageFlags flags, void *data) { 200 | s_signalCbData cbData = *(s_signalCbData*)data; 201 | s_signalEventData *ev = malloc(sizeof(s_signalEventData)); 202 | s_EventDataImMessage *msgData = malloc(sizeof(s_EventDataImMessage)); 203 | 204 | ev->signal = cbData.signal; 205 | msgData->account = account; 206 | msgData->buffer = g_strdup(buffer); 207 | msgData->sender = g_strdup(sender); 208 | 209 | // // TODO: Do not create a convo for chats 210 | // // The first message won't have a conversation, so create it. 211 | if (conv == NULL) { 212 | // This line was stolen from server.c#L624 where immediately after emitting 213 | // received-im-msg it creates a conversation below. 214 | conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, sender); 215 | } 216 | 217 | msgData->conv = conv; 218 | msgData->flags = flags; 219 | ev->freeMe = true; 220 | ev->data = msgData; 221 | signalling_push(ev); 222 | } 223 | 224 | void handleInvited(PurpleAccount *account, const char *inviter, const char *room_name, const char *message, GHashTable *data) { 225 | s_signalEventData *ev = malloc(sizeof(s_signalEventData)); 226 | s_EventDataInvited *msgData = malloc(sizeof(s_EventDataInvited)); 227 | ev->signal = "chat-invite"; 228 | msgData->account = account; 229 | 230 | 231 | if (inviter != NULL) { 232 | msgData->sender = g_strdup(inviter); 233 | } else { 234 | msgData->sender = NULL; 235 | } 236 | 237 | 238 | if (room_name != NULL) { 239 | msgData->roomName = g_strdup(room_name); 240 | } else { 241 | msgData->roomName = NULL; 242 | } 243 | 244 | 245 | if (message != NULL) { 246 | msgData->message = g_strdup(message); 247 | } else { 248 | msgData->message = NULL; 249 | } 250 | 251 | 252 | // XXX: I'm not sure this is the best way to copy the table 253 | // however it seems the only reliable way to do it. 254 | 255 | msgData->inviteProps = g_hash_table_new(g_str_hash, g_str_equal); 256 | 257 | GHashTableIter iter; 258 | gpointer key, val; 259 | g_hash_table_iter_init (&iter, data); 260 | 261 | while (g_hash_table_iter_next (&iter, &key, &val)) 262 | { 263 | g_hash_table_insert(msgData->inviteProps, key, val); 264 | } 265 | 266 | ev->freeMe = true; 267 | ev->data = msgData; 268 | signalling_push(ev); 269 | } 270 | 271 | void handleJoined(PurpleConversation *chat) { 272 | s_signalEventData *ev = malloc(sizeof(s_signalEventData)); 273 | s_EventDataImMessage *msgData = malloc(sizeof(s_EventDataImMessage)); 274 | ev->signal = "chat-joined"; 275 | msgData->conv = chat; //purple_conv_chat_get_conversation(chat); 276 | msgData->account = purple_conversation_get_account(chat); 277 | ev->freeMe = true; 278 | ev->data = msgData; 279 | signalling_push(ev); 280 | } 281 | 282 | void handleAccountConnectionError(PurpleAccount *account, PurpleConnectionError type, char* description) { 283 | s_signalEventData *ev = malloc(sizeof(s_signalEventData)); 284 | s_EventDataConnectionError *msgData = malloc(sizeof(s_EventDataConnectionError)); 285 | msgData->account = account; 286 | msgData->description = g_strdup(description); 287 | msgData->type = type; 288 | ev->data = msgData; 289 | ev->signal = "account-connection-error"; 290 | ev->freeMe = true; 291 | signalling_push(ev); 292 | } 293 | 294 | void* handleUserInfo(PurpleConnection *gc, const char *who, PurpleNotifyUserInfo *user_info) { 295 | GList* entries = purple_notify_user_info_get_entries(user_info); 296 | if (entries == NULL) { 297 | return NULL; 298 | } 299 | 300 | s_signalEventData *ev = malloc(sizeof(s_signalEventData)); 301 | e_UserInfoResponse *msgData = malloc(sizeof(e_UserInfoResponse)); 302 | 303 | msgData->items = NULL; 304 | //g_slist_copy_deep(entries, (GCopyFunc)_copy_user_info_entry, NULL); 305 | GList* l; 306 | for (l = entries; l != NULL; l = l->next) { 307 | PurpleNotifyUserInfoEntry *src = l->data; 308 | e_UserInfoResponseItem *dest; 309 | 310 | if (PURPLE_NOTIFY_USER_INFO_ENTRY_PAIR != purple_notify_user_info_entry_get_type(src)) { 311 | dest = NULL; 312 | continue; 313 | } 314 | dest = malloc(sizeof(e_UserInfoResponseItem)); 315 | 316 | const char* label = purple_notify_user_info_entry_get_label(src); 317 | dest->label = g_strdup(label); 318 | 319 | const char* value = purple_notify_user_info_entry_get_value(src); 320 | dest->value = g_strdup(value); 321 | 322 | msgData->items = g_list_append(msgData->items, dest); 323 | } 324 | msgData->who = g_strdup(who); 325 | msgData->account = purple_connection_get_account(gc); 326 | ev->signal = "user-info-response"; 327 | ev->freeMe = true; 328 | ev->data = msgData; 329 | signalling_push(ev); 330 | 331 | return NULL; 332 | } 333 | 334 | void handleTyping(PurpleAccount *account, const char *name, bool typing) { 335 | s_signalEventData *ev = malloc(sizeof(s_signalEventData)); 336 | e_UserTyping *typingData = malloc(sizeof(e_UserTyping)); 337 | typingData->typing = typing; 338 | typingData->account = account; 339 | typingData->sender = g_strdup(name); 340 | ev->signal = "im-typing"; 341 | ev->freeMe = true; 342 | ev->data = typingData; 343 | signalling_push(ev); 344 | } 345 | 346 | void handleBuddyTyping(PurpleAccount *account, const char *name, void *data) { 347 | handleTyping(account, name, true); 348 | } 349 | 350 | void handleBuddyTypingStopped(PurpleAccount *account, const char *name, void *data) { 351 | handleTyping(account, name, false); 352 | } -------------------------------------------------------------------------------- /src/signalling.h: -------------------------------------------------------------------------------- 1 | #ifndef SIGNALLING_H_INCLUDED 2 | #define SIGNALLING_H_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "bindings/b_accounts.h" 10 | 11 | typedef struct { 12 | char* signal; 13 | } s_signalCbData; 14 | 15 | typedef struct { 16 | char* signal; 17 | gpointer data; 18 | /* Should free the data */ 19 | bool freeMe; 20 | } s_signalEventData; 21 | 22 | typedef enum { 23 | ACCOUNT_GENERIC = 0, 24 | RECEIVED_IM_MSG = 1, 25 | RECEIVED_CHAT_MSG = 2, 26 | CHAT_INVITE = 3 27 | } e_EventObjectType; 28 | 29 | napi_value getJsObjectForSignalEvent(napi_env env, s_signalEventData *eventData); 30 | void signalling_push(s_signalEventData *eventData); 31 | GSList* signalling_pop(); 32 | 33 | /* Specialist callbacks */ 34 | void handleReceivedMessage(PurpleAccount *account, char *sender, char *buffer, PurpleConversation *conv, PurpleMessageFlags flags, void *data); 35 | void handleInvited(PurpleAccount *account, const char *inviter, const char *room_name, const char *message, GHashTable *data); 36 | void handleAccountConnectionError(PurpleAccount *account, PurpleConnectionError type, char* description); 37 | void handleJoined(PurpleConversation *chat); 38 | void* handleUserInfo(PurpleConnection *gc, const char *who, PurpleNotifyUserInfo *user_info); 39 | void handleBuddyTyping(PurpleAccount *account, const char *name, void *data); 40 | void handleBuddyTypingStopped(PurpleAccount *account, const char *name, void *data); 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const libpurple = require('./build/Debug/module'); 2 | console.log(`Libpurple core version:`, libpurple.core.get_version()); 3 | console.log(libpurple); 4 | 5 | let acct; 6 | 7 | setInterval(() => { 8 | if (acct) { 9 | 10 | } 11 | libpurple.helper.pollEvents().forEach((ev) => { 12 | console.log("Got event:", ev); 13 | if (ev.eventName === "received-im-msg"){ 14 | libpurple.messaging.sendIM(acct.handle, ev.sender, ev.message); 15 | } 16 | }); 17 | }, 500); 18 | 19 | libpurple.helper.setupPurple( 20 | { 21 | debugEnabled: 1, 22 | } 23 | ); 24 | console.log("Finished setting up purple!"); 25 | //console.log("Plugin list:", libpurple.plugins.get_protocols()); 26 | acct = libpurple.accounts.find("halfshot@localhost/", "prpl-jabber"); 27 | statusTypes = libpurple.accounts.get_status_types(acct.handle); 28 | console.log("Buddy:", libpurple.buddy.find(acct.handle, "tester@localhost")); 29 | console.log(statusTypes); 30 | //console.log("Acct:", acct); 31 | //console.log("Enabled:", libpurple.accounts.get_enabled(acct.handle)); 32 | //libpurple.accounts.set_enabled(acct.handle, true); 33 | //console.log("Enabled:", libpurple.accounts.get_enabled(acct.handle)); 34 | //libpurple.accounts.new("TestJabber", "prpl-jabber"); 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "node"] 4 | } 5 | } --------------------------------------------------------------------------------