diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..d324c24 --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +source_env .. +use nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8454dc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/nim.cfg diff --git a/README.md b/README.md index d5fbe27..c43b4d1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,71 @@ # telegram_actor -*NOT AN ENDORSEMENT OF TELEGRAM.* +A proxy for communicating with [TDLib](https://core.telegram.org/tdlib/docs/td__json__client_8h.html) over Syndicate using the JSON-serialized object API. + +## Do not use Telegram. + +> It is unsafe as it does not have end-to-end encryption enabled by default for one to one messaging, and it does not have end-to-end-encryption at all for group messaging. It has received a great deal of scrutiny for various design choices including extremely strange cryptographic choices that are suspected of being intentional weaknesses but no smoking gun has been reported. +> +> … +> +> Of note is that some prominent self-identified members of the Internet Freedom social milieu in the United States of America are extremely hostile to Telegram. Usually these critics are strong proponents of Signal and often their criticism appears technically sound. +> +> … + +## Example config +``` +#!/usr/bin/env -S syndicate-server --config + +> + + + +let ?ds = dataspace + +? ?cap> [ + $cap +] + +$ds [ + ?? [ $log ! >>|: $v }> ] + ?? [ $log ! ] + ? ?v [ $log ! ] + ? [ + ! + + ?? [ + ! + ] + + ?? [ + ! + ] + + ?? [ + ! + + ] + + ] +] +``` diff --git a/Tupfile b/Tupfile new file mode 100644 index 0000000..eaaeaaf --- /dev/null +++ b/Tupfile @@ -0,0 +1,3 @@ +include_rules +: |> !nim_lk |> {lockfile} +: {lockfile} |> !nim_cfg |> | ./ diff --git a/Tuprules.tup b/Tuprules.tup new file mode 100644 index 0000000..704673f --- /dev/null +++ b/Tuprules.tup @@ -0,0 +1,4 @@ +include ../syndicate-nim/depends.tup +NIM = $(DIRENV) $(NIM) +NIM_FLAGS += --path:$(TUP_CWD)/../syndicate-nim/src +NIM_GROUPS += $(TUP_CWD)/ diff --git a/config.prs b/config.prs new file mode 100644 index 0000000..7efda1d --- /dev/null +++ b/config.prs @@ -0,0 +1,8 @@ +version 1 . + +TelegramArguments = . + +# Assertion made when the client is ready to process messages. +TelegramReady = . diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..8914209 --- /dev/null +++ b/default.nix @@ -0,0 +1,28 @@ +#{ +# pkgs ? import { }, +#}: + +let + + nixpkgs = builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/2057814051972fa1453ddfb0d98badbea9b83c06.tar.gz"; + sha256 = "1a8jafyawg8mysv787q0cwghkza9kahbbniism7v2rcxas89b575"; + }; + pkgs = import nixpkgs { }; + + inherit (pkgs) + lib + buildNimPackage + pkg-config + tdlib + ; +in + +buildNimPackage { + pname = "telegram_actor"; + version = "unstable"; + src = if lib.inNixShell then null else lib.cleanSource ./.; + buildInputs = [ tdlib ]; + nativeBuildInputs = [ pkg-config ]; + lockFile = ./lock.json; +} diff --git a/lock.json b/lock.json new file mode 100644 index 0000000..f3b16ad --- /dev/null +++ b/lock.json @@ -0,0 +1,142 @@ +{ + "depends": [ + { + "method": "fetchzip", + "packages": [ + "bigints" + ], + "path": "/nix/store/jvrm392g8adfsgf36prgwkbyd7vh5jsw-source", + "rev": "86ea14d31eea9275e1408ca34e6bfe9c99989a96", + "sha256": "15pcpmnk1bnw3k8769rjzcpg00nahyrypwbxs88jnwr4aczp99j4", + "srcDir": "src", + "url": "https://github.com/ehmry/nim-bigints/archive/86ea14d31eea9275e1408ca34e6bfe9c99989a96.tar.gz" + }, + { + "method": "fetchzip", + "packages": [ + "cps" + ], + "path": "/nix/store/8gbhwni0akqskdb3qhn5nfgv6gkdz0vz-source", + "rev": "c90530ac57f98a842b7be969115c6ef08bdcc564", + "sha256": "0h8ghs2fqg68j3jdcg7grnxssmllmgg99kym2w0a3vlwca1zvr62", + "srcDir": "", + "url": "https://github.com/ehmry/cps/archive/c90530ac57f98a842b7be969115c6ef08bdcc564.tar.gz" + }, + { + "method": "fetchzip", + "packages": [ + "getdns" + ], + "path": "/nix/store/x9xmn7w4k6jg8nv5bnx148ibhnsfh362-source", + "rev": "c73cbe288d9f9480586b8fa87f6d794ffb6a6ce6", + "sha256": "1sbgx2x51szr22i72n7c8jglnfmr8m7y7ga0v85d58fwadiv7g6b", + "srcDir": "src", + "url": "https://git.sr.ht/~ehmry/getdns-nim/archive/c73cbe288d9f9480586b8fa87f6d794ffb6a6ce6.tar.gz" + }, + { + "method": "fetchzip", + "packages": [ + "hashlib" + ], + "path": "/nix/store/fav82xdbicvlk34nmcbl89zx99lr3mbs-source", + "rev": "f9455d4be988e14e3dc7933eb7cc7d7c4820b7ac", + "sha256": "1sx6j952lj98629qfgr7ds5aipyw9d6lldcnnqs205wpj4pkcjb3", + "srcDir": "", + "url": "https://github.com/ehmry/hashlib/archive/f9455d4be988e14e3dc7933eb7cc7d7c4820b7ac.tar.gz" + }, + { + "method": "fetchzip", + "packages": [ + "nimcrypto" + ], + "path": "/nix/store/m1qrs3hwcl5a3bkd8f7nbx5p5syid1dh-source", + "rev": "50e8a9b41d221b4eb9981358f65a9ea1a904b471", + "sha256": "0w3pwrg4mjpnzsm16jqg0862v3rgb3pdaxzjrrhwz1rbl04gxa6i", + "srcDir": "", + "url": "https://github.com/cheatfate/nimcrypto/archive/50e8a9b41d221b4eb9981358f65a9ea1a904b471.tar.gz" + }, + { + "method": "fetchzip", + "packages": [ + "npeg" + ], + "path": "/nix/store/xpn694ibgipj8xak3j4bky6b3k0vp7hh-source", + "rev": "ec0cc6e64ea4c62d2aa382b176a4838474238f8d", + "sha256": "1fi9ls3xl20bmv1ikillxywl96i9al6zmmxrbffx448gbrxs86kg", + "srcDir": "src", + "url": "https://github.com/zevv/npeg/archive/ec0cc6e64ea4c62d2aa382b176a4838474238f8d.tar.gz" + }, + { + "method": "fetchzip", + "packages": [ + "preserves" + ], + "path": "/nix/store/hzb7af7lbd4kgd5y4hbgxv1lswig36yj-source", + "rev": "fd498c6457cb9ad2f3179daa40da69eec00326dd", + "sha256": "182xvw04vjw83mlcrkwkip29b44h0v8dapg2014k9011h90mdsj4", + "srcDir": "src", + "url": "https://git.syndicate-lang.org/ehmry/preserves-nim/archive/fd498c6457cb9ad2f3179daa40da69eec00326dd.tar.gz" + }, + { + "method": "fetchzip", + "packages": [ + "stew" + ], + "path": "/nix/store/mqg8qzsbcc8xqabq2yzvlhvcyqypk72c-source", + "rev": "3c91b8694e15137a81ec7db37c6c58194ec94a6a", + "sha256": "17lfhfxp5nxvld78xa83p258y80ks5jb4n53152cdr57xk86y07w", + "srcDir": "", + "url": "https://github.com/status-im/nim-stew/archive/3c91b8694e15137a81ec7db37c6c58194ec94a6a.tar.gz" + }, + { + "method": "fetchzip", + "packages": [ + "syndicate" + ], + "path": "/nix/store/dw30cq9gxz3353zgaq4a36ajq6chvbwc-source", + "rev": "3a4dc1f13392830b587138199643d30fdbec8541", + "sha256": "1mbd17rjm1fsx7d0ckzyjih2nzdjqs52ck9wscqcg9nvf3ib5mvh", + "srcDir": "src", + "url": "https://git.syndicate-lang.org/ehmry/syndicate-nim/archive/3a4dc1f13392830b587138199643d30fdbec8541.tar.gz" + }, + { + "method": "fetchzip", + "packages": [ + "sys" + ], + "path": "/nix/store/syhxsjlsdqfap0hk4qp3s6kayk8cqknd-source", + "rev": "4ef3b624db86e331ba334e705c1aa235d55b05e1", + "sha256": "1q4qgw4an4mmmcbx48l6xk1jig1vc8p9cq9dbx39kpnb0890j32q", + "srcDir": "src", + "url": "https://github.com/ehmry/nim-sys/archive/4ef3b624db86e331ba334e705c1aa235d55b05e1.tar.gz" + }, + { + "method": "fetchzip", + "packages": [ + "taps" + ], + "path": "/nix/store/6y14ia52kr7jyaa0izx37mlablmq9s65-source", + "rev": "8c8572cd971d1283e6621006b310993c632da247", + "sha256": "1dp166bv9x773jmfqppg5i3v3rilgff013vb11yzwcid9l7s3iy8", + "srcDir": "src", + "url": "https://git.sr.ht/~ehmry/nim_taps/archive/8c8572cd971d1283e6621006b310993c632da247.tar.gz" + }, + { + "date": "2024-04-02T15:38:57+01:00", + "deepClone": false, + "fetchLFS": false, + "fetchSubmodules": true, + "hash": "sha256-iZb9aAgYr4FGkqfIg49QWiCqeizIi047kFhugHiP8o0=", + "leaveDotGit": false, + "method": "git", + "packages": [ + "solo5_dispatcher" + ], + "path": "/nix/store/sf5dgj2ljvahcm6my7d61ibda51vnrii-solo5_dispatcher", + "rev": "a7a894a96a2221284012800e6fd32923d83d20bd", + "sha256": "13gjixw80vjqj0xlx2y85ixal82sa27q7j57j9383bqq11lgv5l9", + "srcDir": "pkg", + "url": "https://git.sr.ht/~ehmry/solo5_dispatcher" + } + ] +} diff --git a/src/Tupfile b/src/Tupfile new file mode 100644 index 0000000..cce0260 --- /dev/null +++ b/src/Tupfile @@ -0,0 +1,3 @@ +include_rules +: foreach *.nim | $(SYNDICATE_PROTOCOL) ./ |> !nim_bin |> {bin} +: foreach {bin} |> !assert_built |> diff --git a/src/telegram_actor.nim b/src/telegram_actor.nim new file mode 100644 index 0000000..2bd1f9e --- /dev/null +++ b/src/telegram_actor.nim @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: ☭ Emery Hemingway +# SPDX-License-Identifier: Unlicense + +import + std/[options, streams, tables, times], + preserves, syndicate, syndicate/relays, + ./telegram_actor/[config, td_json_client] + +discard td_execute("""{"@type":"setLogVerbosityLevel", "new_verbosity_level":0}""") + # Disable log vomit to stderr, this isn't a GTK application. + +type State = ref object + facet: Facet + subscribers: Table[int, Cap] + buffer: StringStream + +let state = State( buffer: newStringStream() ) + # Global state object + +let actor = bootActor("main") do (turn: Turn): + state.facet = turn.facet + resolveEnvironment(turn) do (turn: Turn; ds: Cap): + + during(turn, ds, TelegramArguments.grabTypeFlat) do (ds: Cap): + # Extern actors assert themselves here and a ClientId + # is used to isolate their conversations with tdlib. + let client = td_create_client_id() + state.subscribers[client.int] = ds + + onMessage(turn, ds, grabRecord("send", grab())) do (v: Value): + state.buffer.setPosition 0 + state.buffer.writeText(v, textJson) + state.buffer.data.setLen state.buffer.getPosition + td_send(client, state.buffer.data) + # TODO: enforce that the subscriber has not set "@client_id"? + + publish(turn, ds, TelegramReady()) + # Assert to the subscriber that the messages + # it sends will be processed. + + do: + state.subscribers.del client.int + +proc recv(state: State; id: int; recvMsg: Value) = + let subscriber = state.subscribers.getOrDefault id + if not subscriber.isNil: + queueTurn(state.facet) do (turn: Turn): + message(turn, subscriber, recvMsg) + +while actor.running: + # Poll Syndicate and Telegram + const + syndicateTimeout = some initDuration(seconds = 4) + telegramTimeout = 4.0 + syndicate.runOnce(syndicateTimeout) + let cstr = td_receive(telegramTimeout) + if not cstr.isNil: + var + pr = parsePreserves $cstr + clientId: Value + if pr.pop("@client_id".toPreserves, clientId) and clientId.isInteger: + # Pop the client_id out of messages so it isn't exposed to the subscriber. + recv(state, clientId.register, initRecord("recv", pr)) + else: + stderr.writeLine "discarding message without @client_id: ", cstr diff --git a/src/telegram_actor/Tupfile b/src/telegram_actor/Tupfile new file mode 100644 index 0000000..f405409 --- /dev/null +++ b/src/telegram_actor/Tupfile @@ -0,0 +1,2 @@ +include_rules +: foreach ../../*.prs |> !preserves_schema_nim |> %B.nim | ../ diff --git a/src/telegram_actor/config.nim b/src/telegram_actor/config.nim new file mode 100644 index 0000000..179dcf5 --- /dev/null +++ b/src/telegram_actor/config.nim @@ -0,0 +1,18 @@ + +import + preserves + +type + TelegramReady* {.preservesRecord: "telegram-ready".} = object + + TelegramArgumentsField0* {.preservesDictionary.} = object + `dataspace`* {.preservesEmbedded.}: Value + + TelegramArguments* {.preservesRecord: "telegram-client".} = object + `field0`*: TelegramArgumentsField0 + +proc `$`*(x: TelegramReady | TelegramArguments): string = + `$`(toPreserves(x)) + +proc encode*(x: TelegramReady | TelegramArguments): seq[byte] = + encode(toPreserves(x)) diff --git a/src/telegram_actor/td_json_client.nim b/src/telegram_actor/td_json_client.nim new file mode 100644 index 0000000..f8f7a4f --- /dev/null +++ b/src/telegram_actor/td_json_client.nim @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: ☭ Emery Hemingway +# SPDX-License-Identifier: Unlicense + +{.passC: staticExec("pkg-config --cflags tdjson").} +{.passL: staticExec("pkg-config --libs tdjson").} + +{.pragma: import_td, importc, header: "".} + +type + ClientId* = distinct cint + Client* = distinct pointer + LogMessageCallback* = proc (verbosity_level: cint; message: cstring) {.cdecl.} + +proc td_create_client_id*(): ClientId {.import_td.} + +proc td_send*(client_id: ClientId; request: cstring) {.import_td.} + +proc td_receive*(timeout: cdouble): cstring {.import_td.} + +proc td_execute*(request: cstring): cstring {.import_td.} + +proc td_set_log_message_callback*(max_verbosity_level: cint; callback: LogMessageCallback) {.import_td.} + +proc td_json_client_create*(): Client {.import_td, deprecated.} + +proc td_json_client_send*(client: Client; request: cstring) {.import_td, deprecated.} + +proc td_json_client_receive*(client: Client; timeout: cdouble): cstring {.import_td, deprecated.} + +proc td_json_client_execute*(client: Client; request: cstring): cstring {.import_td, deprecated.} + +proc td_json_client_destroy*(client: Client) {.import_td, deprecated.} diff --git a/telegram_actor.nimble b/telegram_actor.nimble new file mode 100644 index 0000000..5680395 --- /dev/null +++ b/telegram_actor.nimble @@ -0,0 +1,8 @@ +version = "20240521" +author = "Emery Hemingway" +description = "Telegram client as a Syndicated Actor" +license = "Unlicense" +srcDir = "src" +bin = @["telegram_actor"] + +requires "syndicate"