general idea and structure together
This commit is contained in:
parent
d0dae05e6e
commit
c0c744ab35
31 changed files with 445 additions and 0 deletions
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial version.
|
30
analysis_options.yaml
Normal file
30
analysis_options.yaml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# This file configures the static analysis results for your project (errors,
|
||||||
|
# warnings, and lints).
|
||||||
|
#
|
||||||
|
# This enables the 'recommended' set of lints from `package:lints`.
|
||||||
|
# This set helps identify many issues that may lead to problems when running
|
||||||
|
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||||
|
# style and format.
|
||||||
|
#
|
||||||
|
# If you want a smaller set of lints you can change this to specify
|
||||||
|
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||||
|
# (the recommended set includes the core lints).
|
||||||
|
# The core lints are also what is used by pub.dev for scoring packages.
|
||||||
|
|
||||||
|
include: package:lints/recommended.yaml
|
||||||
|
|
||||||
|
# Uncomment the following section to specify additional rules.
|
||||||
|
|
||||||
|
# linter:
|
||||||
|
# rules:
|
||||||
|
# - camel_case_types
|
||||||
|
|
||||||
|
# analyzer:
|
||||||
|
# exclude:
|
||||||
|
# - path/to/excluded/files/**
|
||||||
|
|
||||||
|
# For more information about the core and recommended set of lints, see
|
||||||
|
# https://dart.dev/go/core-lints
|
||||||
|
|
||||||
|
# For additional information about configuring this file, see
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
7
bin/torchat_node.dart
Normal file
7
bin/torchat_node.dart
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import 'package:torchat/core_api/torchat_service.dart';
|
||||||
|
|
||||||
|
Future<void> main(List<String> args) async {
|
||||||
|
final service = TorChatService();
|
||||||
|
await service.startNode(); // Boots Tor, gRPC server, discovery loop
|
||||||
|
print('TorChat node running. Press Ctrl-C to exit.');
|
||||||
|
}
|
0
bin/torchat_rest_api.dart
Normal file
0
bin/torchat_rest_api.dart
Normal file
0
lib/core_api/message_handler.dart
Normal file
0
lib/core_api/message_handler.dart
Normal file
0
lib/core_api/torchat_server.dart
Normal file
0
lib/core_api/torchat_server.dart
Normal file
25
lib/core_api/torchat_service.dart
Normal file
25
lib/core_api/torchat_service.dart
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import 'package:torchat/src/p2p/grpc/server/grpc_server.dart';
|
||||||
|
import 'package:torchat/src/p2p/message_queue/outgoing_queue.dart';
|
||||||
|
import 'package:torchat/src/p2p/sync_manager.dart';
|
||||||
|
|
||||||
|
class TorChatService {
|
||||||
|
final OutgoingQueue _queue = OutgoingQueue();
|
||||||
|
late final GrpcServerRunner _grpcServer;
|
||||||
|
late final SyncManager _sync;
|
||||||
|
|
||||||
|
Future<void> startNode() async {
|
||||||
|
// TODO: start Tor/Arti here and open hidden service
|
||||||
|
_grpcServer = GrpcServerRunner();
|
||||||
|
await _grpcServer.start(_queue);
|
||||||
|
|
||||||
|
_sync = SyncManager(_queue);
|
||||||
|
_sync.start();
|
||||||
|
|
||||||
|
// TODO: bootstrap peer discovery loop
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopNode() async {
|
||||||
|
await _grpcServer.stop();
|
||||||
|
_sync.stop();
|
||||||
|
}
|
||||||
|
}
|
0
lib/src/encryption/ed25519_utils.dart
Normal file
0
lib/src/encryption/ed25519_utils.dart
Normal file
0
lib/src/encryption/message_seal.dart
Normal file
0
lib/src/encryption/message_seal.dart
Normal file
0
lib/src/models/peer.dart
Normal file
0
lib/src/models/peer.dart
Normal file
22
lib/src/models/queued_message.dart
Normal file
22
lib/src/models/queued_message.dart
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
class QueuedMessage {
|
||||||
|
final String recipientId;
|
||||||
|
final String message;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final int retryCount;
|
||||||
|
|
||||||
|
QueuedMessage({
|
||||||
|
required this.recipientId,
|
||||||
|
required this.message,
|
||||||
|
DateTime? timestamp,
|
||||||
|
this.retryCount = 0,
|
||||||
|
}) : timestamp = timestamp ?? DateTime.now();
|
||||||
|
|
||||||
|
QueuedMessage copyWith({int? retryCount}) {
|
||||||
|
return QueuedMessage(
|
||||||
|
recipientId: recipientId,
|
||||||
|
message: message,
|
||||||
|
timestamp: timestamp,
|
||||||
|
retryCount: retryCount ?? this.retryCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
0
lib/src/models/user.dart
Normal file
0
lib/src/models/user.dart
Normal file
0
lib/src/p2p/grpc/client/peer_client.dart
Normal file
0
lib/src/p2p/grpc/client/peer_client.dart
Normal file
36
lib/src/p2p/grpc/server/grpc_server.dart
Normal file
36
lib/src/p2p/grpc/server/grpc_server.dart
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:grpc/grpc.dart';
|
||||||
|
import 'package:torchat/src/p2p/message_queue/outgoing_queue.dart';
|
||||||
|
|
||||||
|
class TorChatGrpcServer extends ChatServiceBase {
|
||||||
|
final OutgoingQueue queue;
|
||||||
|
|
||||||
|
TorChatGrpcServer(this.queue);
|
||||||
|
|
||||||
|
// RPC: SendMessage
|
||||||
|
@override
|
||||||
|
Future<ChatMessageReply> sendMessage(
|
||||||
|
ServiceCall call, ChatMessageRequest req) async {
|
||||||
|
// Simply enqueue for now
|
||||||
|
queue.enqueue(req.message);
|
||||||
|
return ChatMessageReply(success: true, messageId: req.message.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement StreamMessages, FetchHistory, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
class GrpcServerRunner {
|
||||||
|
Server? _server;
|
||||||
|
|
||||||
|
Future<void> start(OutgoingQueue queue, {int port = 20900}) async {
|
||||||
|
_server = Server(
|
||||||
|
[TorChatGrpcServer(queue)],
|
||||||
|
const <Interceptor>[],
|
||||||
|
CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]),
|
||||||
|
);
|
||||||
|
await _server!.serve(port: port);
|
||||||
|
stdout.writeln('gRPC server listening on port $port');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stop() => _server?.shutdown() ?? Future.value();
|
||||||
|
}
|
0
lib/src/p2p/message_queue/inbound_handler.dart
Normal file
0
lib/src/p2p/message_queue/inbound_handler.dart
Normal file
14
lib/src/p2p/message_queue/outgoing_queue.dart
Normal file
14
lib/src/p2p/message_queue/outgoing_queue.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
|
||||||
|
class OutgoingQueue {
|
||||||
|
final _list = <Message>[];
|
||||||
|
|
||||||
|
void enqueue(Message msg) => _list.add(msg);
|
||||||
|
bool get hasMessages => _list.isNotEmpty;
|
||||||
|
|
||||||
|
List<Message> drain([int max = 50]) {
|
||||||
|
final drain = _list.take(max).toList();
|
||||||
|
_list.removeRange(0, drain.length);
|
||||||
|
return drain;
|
||||||
|
}
|
||||||
|
}
|
26
lib/src/p2p/sync_manager.dart
Normal file
26
lib/src/p2p/sync_manager.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:torchat/src/p2p/message_queue/outgoing_queue.dart';
|
||||||
|
|
||||||
|
class SyncManager {
|
||||||
|
final OutgoingQueue queue;
|
||||||
|
final Duration interval;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
SyncManager(this.queue, {this.interval = const Duration(seconds: 10)});
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
_timer ??= Timer.periodic(interval, (_) => _flush());
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() => _timer?.cancel();
|
||||||
|
|
||||||
|
Future<void> _flush() async {
|
||||||
|
if (!queue.hasMessages) return;
|
||||||
|
final msgs = queue.drain();
|
||||||
|
// TODO: iterate peers from discovery service and send via gRPC client
|
||||||
|
for (final msg in msgs) {
|
||||||
|
print('[SyncManager] Would send message ${msg.id}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
lib/src/p2p/tor/tor_service.dart
Normal file
0
lib/src/p2p/tor/tor_service.dart
Normal file
0
lib/src/pow/pow_verifier.dart
Normal file
0
lib/src/pow/pow_verifier.dart
Normal file
0
lib/src/relay.dart
Normal file
0
lib/src/relay.dart
Normal file
0
lib/src/repository/group_repository.dart
Normal file
0
lib/src/repository/group_repository.dart
Normal file
0
lib/src/repository/message_repository.dart
Normal file
0
lib/src/repository/message_repository.dart
Normal file
0
lib/src/repository/user_repository.dart
Normal file
0
lib/src/repository/user_repository.dart
Normal file
0
lib/src/storage/spool.dart
Normal file
0
lib/src/storage/spool.dart
Normal file
0
lib/src/storage/sqlite/migrations/v1.sql
Normal file
0
lib/src/storage/sqlite/migrations/v1.sql
Normal file
3
lib/torchat.dart
Normal file
3
lib/torchat.dart
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
int calculate() {
|
||||||
|
return 6 * 7;
|
||||||
|
}
|
75
protospec/chat.proto
Normal file
75
protospec/chat.proto
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package torchat;
|
||||||
|
|
||||||
|
import "user.proto";
|
||||||
|
|
||||||
|
option java_package = "org.torchat.proto";
|
||||||
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
message GroupId {
|
||||||
|
string value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupProfile {
|
||||||
|
GroupId group_id = 1;
|
||||||
|
string display_name = 2;
|
||||||
|
string description = 3;
|
||||||
|
repeated user.UserId members = 4;
|
||||||
|
user.UserId creator = 5;
|
||||||
|
int64 created_at = 6;
|
||||||
|
string avatar_url = 7;
|
||||||
|
repeated string tags = 8;
|
||||||
|
string rules = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupMember {
|
||||||
|
user.UserId user_id = 1;
|
||||||
|
bool is_admin = 2;
|
||||||
|
bool is_muted = 3;
|
||||||
|
int64 joined_at = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupMessage {
|
||||||
|
GroupId group_id = 1;
|
||||||
|
user.UserId sender = 2;
|
||||||
|
string content = 3;
|
||||||
|
int64 timestamp = 4;
|
||||||
|
string message_id = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateGroupRequest {
|
||||||
|
GroupProfile profile = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateGroupReply {
|
||||||
|
GroupId group_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetGroupRequest {
|
||||||
|
GroupId group_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetGroupReply {
|
||||||
|
GroupProfile profile = 1;
|
||||||
|
repeated GroupMember members = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SendGroupMessageRequest {
|
||||||
|
GroupMessage message = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SendGroupMessageReply {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StreamGroupMessagesRequest {
|
||||||
|
GroupId group_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service GroupService {
|
||||||
|
rpc CreateGroup(CreateGroupRequest) returns (CreateGroupReply);
|
||||||
|
rpc GetGroup(GetGroupRequest) returns (GetGroupReply);
|
||||||
|
rpc SendMessage(SendGroupMessageRequest) returns (SendGroupMessageReply);
|
||||||
|
rpc StreamMessages(StreamGroupMessagesRequest) returns (stream GroupMessage);
|
||||||
|
}
|
75
protospec/group.proto
Normal file
75
protospec/group.proto
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package torchat;
|
||||||
|
|
||||||
|
import "user.proto";
|
||||||
|
|
||||||
|
option java_package = "org.torchat.proto";
|
||||||
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
message GroupId {
|
||||||
|
string value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupProfile {
|
||||||
|
GroupId group_id = 1;
|
||||||
|
string display_name = 2;
|
||||||
|
string description = 3;
|
||||||
|
repeated user.UserId members = 4;
|
||||||
|
user.UserId creator = 5;
|
||||||
|
int64 created_at = 6;
|
||||||
|
string avatar_url = 7;
|
||||||
|
repeated string tags = 8;
|
||||||
|
string rules = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupMember {
|
||||||
|
user.UserId user_id = 1;
|
||||||
|
bool is_admin = 2;
|
||||||
|
bool is_muted = 3;
|
||||||
|
int64 joined_at = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupMessage {
|
||||||
|
GroupId group_id = 1;
|
||||||
|
user.UserId sender = 2;
|
||||||
|
string content = 3;
|
||||||
|
int64 timestamp = 4;
|
||||||
|
string message_id = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateGroupRequest {
|
||||||
|
GroupProfile profile = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateGroupReply {
|
||||||
|
GroupId group_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetGroupRequest {
|
||||||
|
GroupId group_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetGroupReply {
|
||||||
|
GroupProfile profile = 1;
|
||||||
|
repeated GroupMember members = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SendGroupMessageRequest {
|
||||||
|
GroupMessage message = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SendGroupMessageReply {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StreamGroupMessagesRequest {
|
||||||
|
GroupId group_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service GroupService {
|
||||||
|
rpc CreateGroup(CreateGroupRequest) returns (CreateGroupReply);
|
||||||
|
rpc GetGroup(GetGroupRequest) returns (GetGroupReply);
|
||||||
|
rpc SendMessage(SendGroupMessageRequest) returns (SendGroupMessageReply);
|
||||||
|
rpc StreamMessages(StreamGroupMessagesRequest) returns (stream GroupMessage);
|
||||||
|
}
|
100
protospec/user.proto
Normal file
100
protospec/user.proto
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// TorChat User Identity & Reputation Spec (v0.1)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// * A user is a long-term Ed25519 key-pair.
|
||||||
|
// * Profile fields are optional and fully signed.
|
||||||
|
// * Multi-device keys are supported via DeviceKey.
|
||||||
|
// * Third-party attestations ("signatures") build a web-of-trust layer.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
package torchat.identity;
|
||||||
|
|
||||||
|
option go_package = "github.com/torchat/proto/identity";
|
||||||
|
option java_package = "org.torchat.identity";
|
||||||
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Primitive types
|
||||||
|
// -------------------------------------------------
|
||||||
|
message PublicKey { bytes value = 1; } // Ed25519 32-byte
|
||||||
|
message Signature { bytes value = 1; } // Ed25519 64-byte on SHA-256 hash
|
||||||
|
message Sha256Hash { bytes value = 1; } // 32-byte hash
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// User Profile (self-signed)
|
||||||
|
// -------------------------------------------------
|
||||||
|
message UserProfile {
|
||||||
|
PublicKey pubkey = 1; // unique master key
|
||||||
|
string nickname = 2; // optional alias
|
||||||
|
string bio = 3; // optional free-text
|
||||||
|
string avatar_url = 4; // optional (ipfs:// or https://)
|
||||||
|
int64 created_at = 5; // epoch millis
|
||||||
|
uint32 version = 6; // profile schema version
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Device key (per-device subkey, signed by master)
|
||||||
|
// -------------------------------------------------
|
||||||
|
message DeviceKey {
|
||||||
|
string device_id = 1; // random UUID / human name
|
||||||
|
PublicKey device_pk = 2; // Ed25519 key for this device
|
||||||
|
int64 created_at = 3;
|
||||||
|
Signature master_sig = 4; // master key sig over hash(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Third-party attestation (web-of-trust)
|
||||||
|
// -------------------------------------------------
|
||||||
|
message Attestation {
|
||||||
|
enum Purpose {
|
||||||
|
GENERIC_TRUST = 0; // default "I trust this user"
|
||||||
|
MODERATOR_ROLE = 1;
|
||||||
|
NOTARY_ROLE = 2; // can co-sign trades, escrow etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
PublicKey signer_pk = 1; // who signs (must be known peer)
|
||||||
|
bytes subject_pk = 2; // user being attested
|
||||||
|
Purpose purpose = 3;
|
||||||
|
string memo = 4; // free text or JSON
|
||||||
|
int64 timestamp = 5;
|
||||||
|
Signature signature = 6; // sig(signer) over hash(all above fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Full signed user record
|
||||||
|
// -------------------------------------------------
|
||||||
|
message SignedUser {
|
||||||
|
UserProfile profile = 1; // MUST contain self-sig
|
||||||
|
Signature self_sig = 2; // sig(master) over hash(profile)
|
||||||
|
repeated DeviceKey devices = 3; // 0+ devices
|
||||||
|
repeated Attestation attestations = 4; // 0+ third-party sigs
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// gRPC Identity Service
|
||||||
|
// -------------------------------------------------
|
||||||
|
service IdentityService {
|
||||||
|
// Register or update your profile. Must include self_sig.
|
||||||
|
rpc RegisterUser (SignedUser) returns (Ack);
|
||||||
|
|
||||||
|
// Add or revoke a device key (master-signed).
|
||||||
|
rpc UpsertDeviceKey (DeviceKey) returns (Ack);
|
||||||
|
|
||||||
|
// Add a third-party attestation.
|
||||||
|
rpc AddAttestation (Attestation) returns (Ack);
|
||||||
|
|
||||||
|
// Get full user record.
|
||||||
|
rpc GetUser (PublicKey) returns (SignedUser);
|
||||||
|
|
||||||
|
// Stream attestations about a user.
|
||||||
|
rpc StreamAttestations(PublicKey) returns (stream Attestation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic acknowledge wrapper
|
||||||
|
message Ack {
|
||||||
|
enum Status { OK = 0; ERROR = 1; }
|
||||||
|
Status status = 1;
|
||||||
|
string message = 2; // optional error / info
|
||||||
|
}
|
21
pubspec.yaml
Normal file
21
pubspec.yaml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name: torchat
|
||||||
|
description: A gRPC-based anonymous chat node (client / server) built with Dart and Tor/Arti
|
||||||
|
version: 0.1.0
|
||||||
|
homepage: https://foss.haveno.com/tor-project/torchat
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.4.0 <4.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
grpc: ^3.2.4
|
||||||
|
protobuf: ^3.1.0
|
||||||
|
convert: ^3.1.1
|
||||||
|
logging: ^1.2.0
|
||||||
|
args: ^2.5.0
|
||||||
|
yaml: ^3.1.2
|
||||||
|
path: ^1.9.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
lints: ^3.0.0
|
||||||
|
test: ^1.25.0
|
||||||
|
build_runner: ^2.4.7
|
||||||
|
protoc_plugin: ^21.1.2
|
8
test/torchat_test.dart
Normal file
8
test/torchat_test.dart
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import 'package:torchat/torchat.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('calculate', () {
|
||||||
|
expect(calculate(), 42);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue