mirror of
https://github.com/Linloir/Simple-TCP-Server.git
synced 2025-12-16 23:48:11 +08:00
initial commit
This commit is contained in:
commit
3eb76d80d9
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# Files and directories created by pub.
|
||||
.dart_tool/
|
||||
.packages
|
||||
|
||||
# Conventional directory for build output.
|
||||
build/
|
||||
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
||||
## 1.0.0
|
||||
|
||||
- Initial version.
|
||||
2
README.md
Normal file
2
README.md
Normal file
@ -0,0 +1,2 @@
|
||||
A sample command-line application with an entrypoint in `bin/`, library code
|
||||
in `lib/`, and example unit test in `test/`.
|
||||
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
|
||||
156
bin/tcp_server.dart
Normal file
156
bin/tcp_server.dart
Normal file
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-06 15:44:16
|
||||
* @LastEditTime : 2022-10-08 23:57:37
|
||||
* @Description :
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:tcp_server/database.dart';
|
||||
import 'package:tcp_server/requesthandler.dart';
|
||||
import 'package:tcp_server/tcpcontroller/controller.dart';
|
||||
import 'package:tcp_server/tcpcontroller/request.dart';
|
||||
import 'package:tcp_server/tcpcontroller/response.dart';
|
||||
|
||||
void main(List<String> arguments) async {
|
||||
await DataBaseHelper().initialize();
|
||||
var tokenMap = <int, Socket>{};
|
||||
var socketMap = <Socket, Future<int>>{};
|
||||
var listenSocket = await ServerSocket.bind('127.0.0.1', 20706);
|
||||
listenSocket.listen(
|
||||
(socket) {
|
||||
var controller = TCPController(socket: socket);
|
||||
controller.stream.listen((request) async {
|
||||
if(request.tokenID == null) {
|
||||
if(socketMap[socket] == null) {
|
||||
socketMap[socket] = (() async => (await DataBaseHelper().createToken()))();
|
||||
}
|
||||
request.tokenID = await socketMap[socket];
|
||||
var tokenResponse = TCPResponse(
|
||||
type: RequestType.token,
|
||||
status: ResponseStatus.ok,
|
||||
body: {
|
||||
"tokenid": request.tokenID
|
||||
}
|
||||
);
|
||||
await socket.addStream(tokenResponse.stream);
|
||||
}
|
||||
tokenMap[request.tokenID!] = tokenMap[request.tokenID!] ?? socket;
|
||||
switch(request.requestType) {
|
||||
case RequestType.checkState: {
|
||||
var response = await onCheckState(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.register: {
|
||||
var response = await onRegister(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.login: {
|
||||
var response = await onLogin(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.logout: {
|
||||
var response = await onLogout(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.profile: {
|
||||
var response = await onFetchProfile(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.modifyProfile: {
|
||||
var response = await onModifyProfile(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.modifyPassword: {
|
||||
var response = await onModifyPassword(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.sendMessage: {
|
||||
//Forword Message
|
||||
var message = request.body['message'] as Map<String, Object?>;
|
||||
await DataBaseHelper().setFetchHistoryFor(
|
||||
tokenID: request.tokenID,
|
||||
newTimeStamp: message['timestamp'] as int
|
||||
);
|
||||
var originUserID = message['userid'] as int;
|
||||
var onlineDevices = await DataBaseHelper().fetchTokenIDsViaUserID(userID: originUserID);
|
||||
for(var device in onlineDevices) {
|
||||
if(device == request.tokenID) {
|
||||
continue;
|
||||
}
|
||||
var targetSocket = tokenMap[device];
|
||||
targetSocket?.write(jsonEncode({
|
||||
'response': 'FORWARDMSG',
|
||||
'body': {
|
||||
"message": message
|
||||
}
|
||||
}));
|
||||
//Update Fetch Histories
|
||||
await DataBaseHelper().setFetchHistoryFor(
|
||||
tokenID: device,
|
||||
newTimeStamp: message['timestamp'] as int
|
||||
);
|
||||
}
|
||||
var targetUserID = message['targetid'] as int;
|
||||
var targetDevices = await DataBaseHelper().fetchTokenIDsViaUserID(userID: targetUserID);
|
||||
for(var device in targetDevices) {
|
||||
//Forward to socket
|
||||
var targetSocket = tokenMap[device];
|
||||
targetSocket?.write(jsonEncode({
|
||||
'response': 'FORWARDMSG',
|
||||
'body': {
|
||||
"message": message
|
||||
}
|
||||
}));
|
||||
//Update Fetch Histories
|
||||
await DataBaseHelper().setFetchHistoryFor(
|
||||
tokenID: device,
|
||||
newTimeStamp: message['timestamp'] as int
|
||||
);
|
||||
}
|
||||
var response = await onSendMessage(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.fetchMessage: {
|
||||
var response = await onFetchMessage(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.fetchFile: {
|
||||
var response = await onFetchFile(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.searchUser: {
|
||||
var response = await onSearchUser(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.fetchContact: {
|
||||
var response = await onFetchContact(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
case RequestType.unknown: {
|
||||
var response = await onUnknownRequest(request, socket);
|
||||
await socket.addStream(response.stream);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
print('[E] Drop out of switch case');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
775
lib/database.dart
Normal file
775
lib/database.dart
Normal file
@ -0,0 +1,775 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-06 16:15:01
|
||||
* @LastEditTime : 2022-10-08 23:54:36
|
||||
* @Description :
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:sqflite_common/sqlite_api.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:tcp_server/tcpcontroller/payload/identity.dart';
|
||||
import 'package:tcp_server/tcpcontroller/payload/message.dart';
|
||||
import 'package:tcp_server/tcpcontroller/payload/userinfo.dart';
|
||||
|
||||
class DataBaseHelper {
|
||||
static final DataBaseHelper _helper = DataBaseHelper._internal();
|
||||
late final Database _database;
|
||||
|
||||
factory DataBaseHelper() {
|
||||
return _helper;
|
||||
}
|
||||
|
||||
DataBaseHelper._internal();
|
||||
|
||||
Future<void> initialize() async {
|
||||
_database = await databaseFactoryFfi.openDatabase(
|
||||
'E:\\database.db',
|
||||
options: OpenDatabaseOptions(
|
||||
version: 1,
|
||||
onCreate: (db, version) {
|
||||
print('[L] Creating Database.');
|
||||
db.execute(
|
||||
'''
|
||||
CREATE TABLE users (
|
||||
userid integer primary key autoincrement,
|
||||
username text not null,
|
||||
passwd text not null,
|
||||
avatar text
|
||||
);
|
||||
create table msgs (
|
||||
userid integer,
|
||||
targetid integer,
|
||||
contenttype text not null,
|
||||
content text not null,
|
||||
timestamp integer,
|
||||
md5encoded text primary key not null
|
||||
);
|
||||
create table contacts (
|
||||
userid integer,
|
||||
targetid integer,
|
||||
primary key (userid, targetid)
|
||||
);
|
||||
create table tokens (
|
||||
tokenid integer primary key autoincrement,
|
||||
createtime integer not null,
|
||||
lastused integer not null
|
||||
);
|
||||
create table bindings (
|
||||
tokenid integer primary key,
|
||||
userid integer
|
||||
);
|
||||
create table histories (
|
||||
tokenid integer,
|
||||
userid integer,
|
||||
lastfetch integer not null,
|
||||
primary key (tokenid, userid)
|
||||
);
|
||||
create table files (
|
||||
filemd5 text primary key not null,
|
||||
dir text not null
|
||||
);
|
||||
create table msgfiles (
|
||||
msgmd5 text not null,
|
||||
filemd5 text not null,
|
||||
primary key (msgmd5, filemd5)
|
||||
);
|
||||
'''
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
//Creates new token
|
||||
Future<int> createToken() async {
|
||||
//Insert new row
|
||||
var row = await _database.rawInsert(
|
||||
'''
|
||||
insert into tokens(createtime, lastused)
|
||||
values (?, ?)
|
||||
''',
|
||||
[
|
||||
DateTime.now().millisecondsSinceEpoch,
|
||||
DateTime.now().millisecondsSinceEpoch
|
||||
]
|
||||
);
|
||||
//Fetch new row
|
||||
var newToken = (await _database.query(
|
||||
'tokens',
|
||||
where: 'rowid = $row',
|
||||
))[0]['tokenid'] as int;
|
||||
//Return token
|
||||
return newToken;
|
||||
}
|
||||
|
||||
Future<UserInfo> checkLoginState({
|
||||
required int? tokenID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
var bindingQueryResult = await _database.query(
|
||||
'bindings natural join users',
|
||||
where: 'tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
);
|
||||
if(bindingQueryResult.isNotEmpty) {
|
||||
return UserInfo(
|
||||
userID: bindingQueryResult[0]['userid'] as int,
|
||||
userName: bindingQueryResult[0]['username'] as String,
|
||||
userAvatar: bindingQueryResult[0]['avatar'] as String?
|
||||
);
|
||||
}
|
||||
else {
|
||||
throw Exception('User not logged in');
|
||||
}
|
||||
}
|
||||
|
||||
Future<UserInfo> logIn({
|
||||
required UserIdentity identity,
|
||||
required int? tokenID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
var userIdentities = await _database.query(
|
||||
'users',
|
||||
where: 'username = ?',
|
||||
whereArgs: [
|
||||
identity.userName
|
||||
]
|
||||
);
|
||||
if(userIdentities.isNotEmpty) {
|
||||
var user = userIdentities[0];
|
||||
if(user['passwd'] == identity.userPasswd) {
|
||||
//Query for existed token binding
|
||||
var existBindings = await _database.query(
|
||||
'bindings',
|
||||
where: 'tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
);
|
||||
if(existBindings.isEmpty) {
|
||||
//Add new binding
|
||||
await _database.insert(
|
||||
'bindings',
|
||||
{
|
||||
'tokenid': tokenID,
|
||||
'userid': user['userid']
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
//Update token binding
|
||||
await _database.update(
|
||||
'bindings',
|
||||
{
|
||||
'tokenid': tokenID,
|
||||
'userid': user['userid']
|
||||
},
|
||||
where: 'tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
);
|
||||
}
|
||||
return UserInfo(
|
||||
userID: user['userid'] as int,
|
||||
userName: user['username'] as String,
|
||||
userAvatar: user['avatar'] as String?
|
||||
);
|
||||
}
|
||||
else {
|
||||
throw Exception('Invalid password');
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw Exception('User not found');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logOut({
|
||||
required int? tokenID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
//Delete binding
|
||||
await _database.delete(
|
||||
'bindings',
|
||||
where: 'tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Future<UserInfo> registerUser({
|
||||
required UserIdentity identity,
|
||||
required int? tokenID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
//Insert into users
|
||||
try {
|
||||
await _database.insert(
|
||||
'users',
|
||||
{
|
||||
'username': identity.userName,
|
||||
'passwd': identity.userPasswd,
|
||||
'avatar': null
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.rollback
|
||||
);
|
||||
} catch (conflict) {
|
||||
throw Exception(['Database failure', conflict.toString()]);
|
||||
}
|
||||
|
||||
//Get new userid
|
||||
var newUserID = (await _database.query(
|
||||
'users',
|
||||
where: 'username = ?',
|
||||
whereArgs: [
|
||||
identity.userName
|
||||
]
|
||||
))[0]['userid'] as int;
|
||||
|
||||
//Insert into bindings
|
||||
await _database.insert(
|
||||
'bindings',
|
||||
{
|
||||
'tokenid': tokenID,
|
||||
'userid': newUserID
|
||||
}
|
||||
);
|
||||
|
||||
return UserInfo(
|
||||
userID: newUserID,
|
||||
userName: identity.userName,
|
||||
userAvatar: null
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> modifyUserPassword({
|
||||
required UserIdentity newIdentity,
|
||||
required int? tokenID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
//Find current binded user
|
||||
var currentUserQueryResult = await _database.query(
|
||||
'bindings natural join users',
|
||||
where: 'tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
);
|
||||
if(currentUserQueryResult.isEmpty) {
|
||||
throw Exception('User not logged in');
|
||||
}
|
||||
var currentUser = currentUserQueryResult[0];
|
||||
|
||||
//Verify user identity
|
||||
if(currentUser['passwd'] as String != newIdentity.userPasswd) {
|
||||
throw Exception('Wrong password');
|
||||
}
|
||||
else {
|
||||
try {
|
||||
//Modify database
|
||||
await _database.update(
|
||||
'users',
|
||||
{
|
||||
'passwd': newIdentity.userPasswdNew
|
||||
},
|
||||
where: 'userid = ${currentUser['userid'] as int}',
|
||||
conflictAlgorithm: ConflictAlgorithm.rollback
|
||||
);
|
||||
} catch (conflict) {
|
||||
throw Exception(['Database failure', conflict.toString()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Returns a list of unfetched messages in JSON format
|
||||
Future<List<Message>> fetchMessagesFor({
|
||||
required int? tokenID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
//Find userID and last fetched time
|
||||
var userIdQueryResult = await _database.query(
|
||||
'bindings natural left outer join histories',
|
||||
columns: ['userid', 'lastfetch'],
|
||||
where: 'tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
);
|
||||
if(userIdQueryResult.isEmpty) {
|
||||
throw Exception('User not logged in');
|
||||
}
|
||||
var userID = userIdQueryResult[0]['userid'] as int;
|
||||
var lastFetch = userIdQueryResult[0]['lastfetch'] as int?;
|
||||
if(lastFetch == null) {
|
||||
//First fetch, add to fetch history
|
||||
await _database.insert(
|
||||
'histories',
|
||||
{
|
||||
'tokenid': tokenID,
|
||||
'userid': userID,
|
||||
'lastfetch': 0
|
||||
}
|
||||
);
|
||||
lastFetch = 0;
|
||||
}
|
||||
|
||||
//Fetch unfetched messages
|
||||
var unfetchMsgQueryResult = await _database.query(
|
||||
'msgs',
|
||||
where: '(userid = ? or targetid = ?) and timestamp > ?',
|
||||
whereArgs: [
|
||||
userID,
|
||||
userID,
|
||||
lastFetch
|
||||
],
|
||||
orderBy: 'timestamp desc'
|
||||
);
|
||||
var unfetchMessages = unfetchMsgQueryResult.map((message) {
|
||||
return Message(
|
||||
userid: message['userid'] as int,
|
||||
targetid: message['targetid'] as int,
|
||||
contenttype: MessageType.fromStringLiteral(
|
||||
message['contenttype'] as String
|
||||
),
|
||||
content: message['content'] as String,
|
||||
timestamp: message['timestamp'] as int,
|
||||
md5encoded: message['md5encoded'] as String
|
||||
);
|
||||
}).toList();
|
||||
|
||||
//Set new fetch history
|
||||
if(unfetchMsgQueryResult.isNotEmpty) {
|
||||
await _database.update(
|
||||
'histories',
|
||||
{
|
||||
'lastfetch': unfetchMsgQueryResult[0]['timestamp']
|
||||
},
|
||||
where: 'tokenid = ? and userid = ?',
|
||||
whereArgs: [
|
||||
tokenID,
|
||||
userID
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
//return result
|
||||
return unfetchMessages;
|
||||
}
|
||||
|
||||
Future<void> setFetchHistoryFor({
|
||||
required int? tokenID,
|
||||
required int newTimeStamp
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
//Get current userid
|
||||
var bindingQueryResult = await _database.query(
|
||||
'bindings natural left outer join histories',
|
||||
where: 'bindings.tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
);
|
||||
if(bindingQueryResult.isEmpty) {
|
||||
//Be silence on err
|
||||
return;
|
||||
}
|
||||
var userID = bindingQueryResult[0]['userid'] as int;
|
||||
|
||||
//Check for fetch history
|
||||
var lastFetch = bindingQueryResult[0]['lastfetch'] as int?;
|
||||
if(lastFetch == null) {
|
||||
//First fetch, add to fetch history
|
||||
await _database.insert(
|
||||
'histories',
|
||||
{
|
||||
'tokenid': tokenID,
|
||||
'userid': userID,
|
||||
'lastfetch': newTimeStamp
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
//Update fetch history
|
||||
await _database.update(
|
||||
'histories',
|
||||
{
|
||||
'lastfetch': newTimeStamp
|
||||
},
|
||||
where: 'tokenid = ? and userid = ?',
|
||||
whereArgs: [
|
||||
tokenID,
|
||||
userID
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> storeMessage({
|
||||
required Message msg,
|
||||
String? fileMd5
|
||||
}) async {
|
||||
try {
|
||||
await _database.insert(
|
||||
'msgs',
|
||||
{
|
||||
'userid': msg.senderID,
|
||||
'targetid': msg.receiverID,
|
||||
'contenttype': msg.contentType.literal,
|
||||
'content': msg.content,
|
||||
'timestamp': msg.timestamp,
|
||||
'md5encoded': msg.md5encoded,
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
print('[E] Database failure on message storage:');
|
||||
print('[>] $err');
|
||||
}
|
||||
if(msg.contentType == MessageType.file) {
|
||||
if(fileMd5 == null) {
|
||||
await _database.delete(
|
||||
'msgs',
|
||||
where: 'md5encoded = ?',
|
||||
whereArgs: [
|
||||
msg.md5encoded
|
||||
]
|
||||
);
|
||||
throw Exception('Missing file for message');
|
||||
}
|
||||
await _database.insert(
|
||||
'msgfiles',
|
||||
{
|
||||
'msgmd5': msg.md5encoded,
|
||||
'filemd5': fileMd5
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> storeFile({
|
||||
required File? tempFile,
|
||||
required String? fileMd5
|
||||
}) async {
|
||||
if(tempFile == null || fileMd5 == null) {
|
||||
throw Exception('Missing file parts');
|
||||
}
|
||||
var filePath = '${Directory.current.path}\\$fileMd5';
|
||||
await tempFile.copy(filePath);
|
||||
tempFile.delete();
|
||||
try {
|
||||
await _database.insert(
|
||||
'files',
|
||||
{
|
||||
'filemd5': fileMd5,
|
||||
'dir': filePath
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.rollback
|
||||
);
|
||||
} catch (conflict) {
|
||||
throw Exception(['Database failure', conflict.toString()]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> fetchFilePath({
|
||||
required String msgMd5
|
||||
}) async {
|
||||
var queryResult = await _database.query(
|
||||
'msgfiles natural join files',
|
||||
where: 'msgfile.msgmd5 = ?',
|
||||
whereArgs: [
|
||||
msgMd5
|
||||
]
|
||||
);
|
||||
if(queryResult.isEmpty) {
|
||||
throw Exception('File not found');
|
||||
}
|
||||
return queryResult[0]['filedir'] as String;
|
||||
}
|
||||
|
||||
Future<UserInfo> fetchUserInfoViaToken({
|
||||
required int? tokenID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
//Find current binded userID
|
||||
var currentUserQueryResult = (await _database.query(
|
||||
'bindings natural join users',
|
||||
where: 'bindings.tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
));
|
||||
if(currentUserQueryResult.isEmpty) {
|
||||
throw Exception('User not logged in');
|
||||
}
|
||||
var currentUser = currentUserQueryResult[0];
|
||||
|
||||
return UserInfo(
|
||||
userID: currentUser['userid'] as int,
|
||||
userName: currentUser['username'] as String,
|
||||
userAvatar: currentUser['avatar'] as String?
|
||||
);
|
||||
}
|
||||
|
||||
Future<UserInfo> modifyUserInfo({
|
||||
required UserInfo userInfo,
|
||||
required int? tokenID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
//Find current binded userID
|
||||
var currentUserIDQueryResult = (await _database.query(
|
||||
'bindings',
|
||||
where: 'tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
));
|
||||
if(currentUserIDQueryResult.isEmpty) {
|
||||
throw Exception('User not logged in');
|
||||
}
|
||||
var currentUserID = currentUserIDQueryResult[0]['userid'] as int;
|
||||
|
||||
//Update database
|
||||
try {
|
||||
await _database.update(
|
||||
'users',
|
||||
{
|
||||
'username': userInfo.userName,
|
||||
'avatar': userInfo.userAvatar
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.rollback
|
||||
);
|
||||
} catch (conflict) {
|
||||
throw Exception(['Database failure', conflict.toString()]);
|
||||
}
|
||||
|
||||
//Return result
|
||||
return UserInfo(
|
||||
userID: currentUserID,
|
||||
userName: userInfo.userName,
|
||||
userAvatar: userInfo.userAvatar
|
||||
);
|
||||
}
|
||||
|
||||
Future<UserInfo> fetchUserInfoViaUsername({
|
||||
required String username
|
||||
}) async {
|
||||
var targetUserQueryResult = await _database.query(
|
||||
'users',
|
||||
columns: [
|
||||
'userid',
|
||||
'username',
|
||||
'avatar'
|
||||
],
|
||||
where: 'username = ?',
|
||||
whereArgs: [
|
||||
username
|
||||
]
|
||||
);
|
||||
if(targetUserQueryResult.isNotEmpty) {
|
||||
return UserInfo(
|
||||
userID: targetUserQueryResult[0]['userid'] as int,
|
||||
userName: targetUserQueryResult[0]['username'] as String,
|
||||
userAvatar: targetUserQueryResult[0]['avatar'] as String?
|
||||
);
|
||||
}
|
||||
else {
|
||||
throw Exception('User not found');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<UserInfo>> fetchContact({
|
||||
required int? tokenID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
//Find current binded userID
|
||||
var currentUserID = (await _database.query(
|
||||
'bindings',
|
||||
where: 'tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
))[0]['userid'] as int;
|
||||
|
||||
//Fetch all contacts
|
||||
var contactsQueryResult = await _database.query(
|
||||
'contacts as I join contacts as P on I.targetid = P.userid join users on I.targetid = users.userid',
|
||||
columns: ['I.targetid as userid', 'users.username as username', 'users.avatar as avatar'],
|
||||
where: 'I.userid = P.targetid and I.userid = ?',
|
||||
whereArgs: [
|
||||
currentUserID
|
||||
]
|
||||
);
|
||||
|
||||
//Convert to encodable objects
|
||||
var contactsEncodable = contactsQueryResult.map((contact) {
|
||||
return UserInfo(
|
||||
userID: contact['userid'] as int,
|
||||
userName: contact['username'] as String,
|
||||
userAvatar: contact['avatar'] as String?
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return contactsEncodable;
|
||||
}
|
||||
|
||||
Future<List<UserInfo>> fetchPendingContacts({
|
||||
required int? tokenID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
//Find current binded userID
|
||||
var currentUserID = (await _database.query(
|
||||
'bindings',
|
||||
where: 'tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
))[0]['userid'] as int;
|
||||
|
||||
//Fetch pending contacts
|
||||
var contactsQueryResult = await _database.query(
|
||||
'contacts join users on contacts.targetid = users.userid',
|
||||
columns: ['contacts.targetid as userid', 'users.username as username', 'users.avatar as avatar'],
|
||||
where: '''contacts.userid = ? and not exists (
|
||||
select * from contacts as S
|
||||
where contacts.targetid = S.userid and contacts.userid = S.targetid
|
||||
)''',
|
||||
whereArgs: [
|
||||
currentUserID
|
||||
]
|
||||
);
|
||||
|
||||
//Convert to encodable objects
|
||||
var contactsEncodable = contactsQueryResult.map((contact) {
|
||||
return UserInfo(
|
||||
userID: contact['userid'] as int,
|
||||
userName: contact['username'] as String,
|
||||
userAvatar: contact['avatar'] as String?
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return contactsEncodable;
|
||||
}
|
||||
|
||||
Future<List<UserInfo>> fetchRequestingContacts({
|
||||
required int? tokenID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
//Find current binded userID
|
||||
var currentUserID = (await _database.query(
|
||||
'bindings',
|
||||
where: 'tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
))[0]['userid'] as int;
|
||||
|
||||
//Fetch pending contacts
|
||||
var contactsQueryResult = await _database.query(
|
||||
'contacts join users on contacts.userid = users.userid',
|
||||
columns: ['contacts.userid as userid', 'users.username as username', 'users.avatar as avatar'],
|
||||
where: '''contacts.targetid = ? and not exists (
|
||||
select * from contacts as S
|
||||
where contacts.targetid = S.userid and contacts.userid = S.targetid
|
||||
)''',
|
||||
whereArgs: [
|
||||
currentUserID
|
||||
]
|
||||
);
|
||||
|
||||
//Convert to encodable objects
|
||||
var contactsEncodable = contactsQueryResult.map((contact) {
|
||||
return UserInfo(
|
||||
userID: contact['userid'] as int,
|
||||
userName: contact['username'] as String,
|
||||
userAvatar: contact['avatar'] as String?
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return contactsEncodable;
|
||||
}
|
||||
|
||||
Future<void> addContact({
|
||||
required int? tokenID,
|
||||
required int userID
|
||||
}) async {
|
||||
if(tokenID == null) {
|
||||
throw Exception('Invalid device token');
|
||||
}
|
||||
|
||||
//Find current binded userID
|
||||
var currentUserID = (await _database.query(
|
||||
'bindings',
|
||||
where: 'tokenid = ?',
|
||||
whereArgs: [
|
||||
tokenID
|
||||
]
|
||||
))[0]['userid'] as int;
|
||||
|
||||
//Add contacts
|
||||
await _database.insert(
|
||||
'contacts',
|
||||
{
|
||||
'userid': currentUserID,
|
||||
'targetid': userID
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<int>> fetchTokenIDsViaUserID({
|
||||
required int userID
|
||||
}) async {
|
||||
var tokenIDQueryResult = await _database.query(
|
||||
'bindings',
|
||||
where: 'userid = ?',
|
||||
whereArgs: [
|
||||
userID
|
||||
]
|
||||
);
|
||||
|
||||
return tokenIDQueryResult.map((token) {
|
||||
return token['tokenid'] as int;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
272
lib/requesthandler.dart
Normal file
272
lib/requesthandler.dart
Normal file
@ -0,0 +1,272 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-08 20:52:48
|
||||
* @LastEditTime : 2022-10-08 23:39:59
|
||||
* @Description :
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:tcp_server/database.dart';
|
||||
import 'package:tcp_server/tcpcontroller/payload/identity.dart';
|
||||
import 'package:tcp_server/tcpcontroller/payload/message.dart';
|
||||
import 'package:tcp_server/tcpcontroller/payload/userinfo.dart';
|
||||
import 'package:tcp_server/tcpcontroller/request.dart';
|
||||
import 'package:tcp_server/tcpcontroller/response.dart';
|
||||
|
||||
Future<TCPResponse> onCheckState(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
var userInfo = await DataBaseHelper().checkLoginState(tokenID: request.tokenID);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok,
|
||||
body: userInfo.jsonObject
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onRegister(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
UserIdentity identity = UserIdentity.fromJSONObject(request.body);
|
||||
var newUserInfo = await DataBaseHelper().registerUser(
|
||||
identity: identity,
|
||||
tokenID: request.tokenID
|
||||
);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok,
|
||||
body: newUserInfo.jsonObject
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onLogin(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
var userInfo = await DataBaseHelper().logIn(
|
||||
identity: UserIdentity.fromJSONObject(request.body),
|
||||
tokenID: request.tokenID
|
||||
);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok,
|
||||
body: userInfo.jsonObject
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onLogout(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
await DataBaseHelper().logOut(tokenID: request.tokenID);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok,
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onFetchProfile(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
var userInfo = await DataBaseHelper().fetchUserInfoViaToken(tokenID: request.tokenID);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok,
|
||||
body: userInfo.jsonObject
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onModifyPassword(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
await DataBaseHelper().modifyUserPassword(
|
||||
newIdentity: UserIdentity.fromJSONObject(request.body),
|
||||
tokenID: request.tokenID
|
||||
);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onModifyProfile(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
var newUserInfo = await DataBaseHelper().modifyUserInfo(
|
||||
userInfo: UserInfo.fromJSONObject(request.body),
|
||||
tokenID: request.tokenID
|
||||
);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok,
|
||||
body: newUserInfo.jsonObject
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onSendMessage(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
var message = Message.fromJSONObject(request.body);
|
||||
if(message.contentType == MessageType.file) {
|
||||
await DataBaseHelper().storeFile(
|
||||
tempFile: request.payload,
|
||||
fileMd5: message.fileMd5
|
||||
);
|
||||
}
|
||||
await DataBaseHelper().storeMessage(
|
||||
msg: message,
|
||||
fileMd5: message.fileMd5
|
||||
);
|
||||
return TCPResponse(
|
||||
type: RequestType.sendMessage,
|
||||
status: ResponseStatus.ok,
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onFetchMessage(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
var messages = await DataBaseHelper().fetchMessagesFor(tokenID: request.tokenID);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok,
|
||||
body: {
|
||||
'messages': messages.map((e) => e.jsonObject)
|
||||
}
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onFetchFile(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
var filePath = await DataBaseHelper().fetchFilePath(msgMd5: request.body['msgmd5'] as String);
|
||||
var file = File(filePath);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok,
|
||||
payload: file
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onSearchUser(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
var userInfo = await DataBaseHelper().fetchUserInfoViaUsername(username: request.body['username'] as String);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok,
|
||||
body: userInfo.jsonObject
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onAddContact(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
await DataBaseHelper().addContact(tokenID: request.tokenID, userID: request.body['userid'] as int);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onFetchContact(TCPRequest request, Socket socket) async {
|
||||
try {
|
||||
var contacts = await DataBaseHelper().fetchContact(tokenID: request.tokenID);
|
||||
var pendingContacts = await DataBaseHelper().fetchPendingContacts(tokenID: request.tokenID);
|
||||
var requestingContacts = await DataBaseHelper().fetchRequestingContacts(tokenID: request.tokenID);
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.ok,
|
||||
body: {
|
||||
"contacts": contacts.map((e) => e.jsonObject),
|
||||
"pending": pendingContacts.map((e) => e.jsonObject),
|
||||
"requesting": requestingContacts.map((e) => e.jsonObject)
|
||||
}
|
||||
);
|
||||
} on Exception catch (exception) {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: exception.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TCPResponse> onUnknownRequest(TCPRequest request, Socket socket) async {
|
||||
return TCPResponse(
|
||||
type: request.requestType,
|
||||
status: ResponseStatus.err,
|
||||
errInfo: 'Unkown request'
|
||||
);
|
||||
}
|
||||
125
lib/tcpcontroller/controller.dart
Normal file
125
lib/tcpcontroller/controller.dart
Normal file
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-08 15:10:04
|
||||
* @LastEditTime : 2022-10-08 23:11:24
|
||||
* @Description :
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:tcp_server/tcpcontroller/request.dart';
|
||||
|
||||
class TCPController {
|
||||
final Socket socket;
|
||||
|
||||
//Stores the incoming bytes of the TCP connection temporarily
|
||||
final Uint8List buffer = Uint8List(0);
|
||||
|
||||
//Byte length for json object
|
||||
int requestLength = 0;
|
||||
//Byte length for subsequent data of the json object
|
||||
int payloadLength = 0;
|
||||
|
||||
//Construct a stream which emits events on intact requests
|
||||
StreamController<Uint8List> _requestStreamController = StreamController()..close();
|
||||
|
||||
//Construct a payload stream which forward the incoming byte into temp file
|
||||
StreamController<Uint8List> _payloadStreamController = StreamController()..close();
|
||||
|
||||
//Provide a request stream for caller functions to listen on
|
||||
final StreamController<TCPRequest> _streamController = StreamController();
|
||||
Stream<TCPRequest> get stream => _streamController.stream;
|
||||
|
||||
TCPController({
|
||||
required this.socket
|
||||
}) {
|
||||
socket.listen(socketHandler);
|
||||
}
|
||||
|
||||
//Listen to the incoming stream and emits event whenever there is a intact request
|
||||
void socketHandler(Uint8List fetchedData) {
|
||||
//Put incoming data into buffer
|
||||
buffer.addAll(fetchedData);
|
||||
//Consume buffer until it's not enough for first 8 byte of a message
|
||||
while(true) {
|
||||
if(requestLength == 0 && payloadLength == 0) {
|
||||
//New request
|
||||
if(buffer.length > 8) {
|
||||
//Buffered data has more than 8 bytes, enough to read request length and body length
|
||||
requestLength = buffer.sublist(0, 4).buffer.asByteData().getInt32(0);
|
||||
payloadLength = buffer.sublist(4, 8).buffer.asByteData().getInt32(0);
|
||||
//Clear the length indicator bytes
|
||||
buffer.removeRange(0, 8);
|
||||
//Create temp file to read payload (might be huge)
|
||||
var tempFile = File('./temp${DateTime.now().microsecondsSinceEpoch}.temp')..createSync();
|
||||
//Initialize payload transmission controller
|
||||
_payloadStreamController = StreamController();
|
||||
//Bind file to stream
|
||||
_payloadStreamController.stream.listen((data) {
|
||||
tempFile.writeAsBytes(data, mode: FileMode.append);
|
||||
});
|
||||
//Bind request construction on stream
|
||||
_requestStreamController = StreamController();
|
||||
_requestStreamController.stream.listen((requestBytes) {
|
||||
//When request stream is closed by controller
|
||||
var request = TCPRequest(requestBytes, tempFile);
|
||||
_payloadStreamController.done.then((_) {
|
||||
_streamController.add(request);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
//Buffered data is not long enough
|
||||
//Do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
//Currently awaiting full transmission
|
||||
if(requestLength > 0) {
|
||||
//Currently processing on a request
|
||||
if(buffer.length > requestLength) {
|
||||
//Got intact request json
|
||||
//Emit request buffer through stream
|
||||
_requestStreamController.add(buffer.sublist(0, requestLength));
|
||||
_requestStreamController.close();
|
||||
//Remove proccessed buffer
|
||||
buffer.removeRange(0, requestLength);
|
||||
//Clear awaiting request length
|
||||
requestLength = 0;
|
||||
}
|
||||
else {
|
||||
//Got part of request json
|
||||
//do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
//Currently processing on a payload
|
||||
if(buffer.length >= payloadLength) {
|
||||
//Last few bytes to emit
|
||||
//Send the last few bytes to stream
|
||||
_payloadStreamController.add(buffer.sublist(0, payloadLength));
|
||||
//Clear buffer
|
||||
buffer.removeRange(0, payloadLength);
|
||||
//Set payload length to zero
|
||||
payloadLength = 0;
|
||||
//Close the payload transmission stream
|
||||
_payloadStreamController.close();
|
||||
}
|
||||
else {
|
||||
//Part of payload
|
||||
//Transmit all to stream
|
||||
_payloadStreamController.add(buffer);
|
||||
//Reduce payload bytes left
|
||||
payloadLength -= buffer.length;
|
||||
//Clear buffer
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
lib/tcpcontroller/payload/identity.dart
Normal file
26
lib/tcpcontroller/payload/identity.dart
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-08 16:16:10
|
||||
* @LastEditTime : 2022-10-08 22:36:19
|
||||
* @Description :
|
||||
*/
|
||||
|
||||
class UserIdentity {
|
||||
final Map<String, Object?> _data;
|
||||
|
||||
UserIdentity({
|
||||
required String userName,
|
||||
required String userPasswdEncoded,
|
||||
String? userPasswdEncodedNew
|
||||
}): _data = {
|
||||
"username": userName,
|
||||
"passwd": userPasswdEncoded,
|
||||
"newPasswd": userPasswdEncodedNew
|
||||
};
|
||||
UserIdentity.fromJSONObject(Map<String, Object?> data): _data = data;
|
||||
|
||||
String get userName => _data['username'] as String;
|
||||
String get userPasswd => _data['passwd'] as String;
|
||||
String? get userPasswdNew => _data['newPasswd'] as String?;
|
||||
Map<String, Object?> get jsonObject => _data;
|
||||
}
|
||||
59
lib/tcpcontroller/payload/message.dart
Normal file
59
lib/tcpcontroller/payload/message.dart
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-08 16:16:19
|
||||
* @LastEditTime : 2022-10-08 23:20:36
|
||||
* @Description : Message Info Payload
|
||||
*/
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:tcp_server/utils/typeconverter.dart';
|
||||
|
||||
enum MessageType {
|
||||
plaintext('plaintext'),
|
||||
file('file'),
|
||||
image('image');
|
||||
|
||||
factory MessageType.fromStringLiteral(String value) {
|
||||
return MessageType.values.firstWhere((element) => element._value == value);
|
||||
}
|
||||
const MessageType(String value): _value = value;
|
||||
final String _value;
|
||||
String get literal => _value;
|
||||
}
|
||||
|
||||
class Message {
|
||||
final Map<String, Object?> _data;
|
||||
|
||||
Message({
|
||||
required int userid,
|
||||
required int targetid,
|
||||
required MessageType contenttype,
|
||||
required String content,
|
||||
required int timestamp,
|
||||
String? md5encoded,
|
||||
String? filemd5
|
||||
}): _data = {
|
||||
"userid": userid,
|
||||
"targetid": targetid,
|
||||
"contenttype": contenttype.literal,
|
||||
"content": content,
|
||||
"timestamp": timestamp,
|
||||
"md5encoded": md5encoded ?? md5.convert(
|
||||
intToUint8List(userid)
|
||||
..addAll(intToUint8List(targetid))
|
||||
..addAll(intToUint8List(timestamp))
|
||||
..addAll(content.codeUnits)
|
||||
),
|
||||
"filemd5": filemd5
|
||||
};
|
||||
Message.fromJSONObject(Map<String, Object?> data): _data = data;
|
||||
|
||||
int get senderID => _data['userid'] as int;
|
||||
int get receiverID => _data['targetid'] as int;
|
||||
MessageType get contentType => MessageType.fromStringLiteral(_data['contenttype'] as String);
|
||||
String get content => _data['content'] as String;
|
||||
int get timestamp => _data['timestamp'] as int;
|
||||
String get md5encoded => _data['md5encoded'] as String;
|
||||
String? get fileMd5 => _data['filemd5'] as String?;
|
||||
Map<String, Object?> get jsonObject => _data;
|
||||
}
|
||||
26
lib/tcpcontroller/payload/userinfo.dart
Normal file
26
lib/tcpcontroller/payload/userinfo.dart
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-08 16:15:17
|
||||
* @LastEditTime : 2022-10-08 22:35:53
|
||||
* @Description : User Info Payload
|
||||
*/
|
||||
|
||||
class UserInfo {
|
||||
final Map<String, Object?> _data;
|
||||
|
||||
UserInfo({
|
||||
required int userID,
|
||||
required String userName,
|
||||
String? userAvatar
|
||||
}): _data = {
|
||||
"userid": userID,
|
||||
"username": userName,
|
||||
"avatar": userAvatar
|
||||
};
|
||||
UserInfo.fromJSONObject(Map<String, Object?> data): _data = data;
|
||||
|
||||
int get userID => _data['userid'] as int;
|
||||
String get userName => _data['username'] as String;
|
||||
String? get userAvatar => _data['avatar'] as String?;
|
||||
Map<String, Object?> get jsonObject => _data;
|
||||
}
|
||||
51
lib/tcpcontroller/request.dart
Normal file
51
lib/tcpcontroller/request.dart
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-08 15:14:26
|
||||
* @LastEditTime : 2022-10-08 23:52:50
|
||||
* @Description :
|
||||
*/
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
enum RequestType {
|
||||
token ('TOKEN'), //Only exists when server is sending message
|
||||
checkState ('STATE'), //Check login state for device token
|
||||
register ('REGISTER'), //Register new user
|
||||
login ('LOGIN'), //Login via username and password
|
||||
logout ('LOGOUT'), //Logout for current device token
|
||||
profile ('PROFILE'), //Fetch current logged in user profile
|
||||
modifyPassword('MODIFYPASSWD'), //Modify user password
|
||||
modifyProfile ('MODIFYPROFILE'), //Modify user profile
|
||||
sendMessage ('SENDMSG'), //Send message
|
||||
fetchMessage ('FETCHMSG'), //Fetch message
|
||||
findFile ('FINDFILE'), //Find file by md5 before transmitting the file
|
||||
fetchFile ('FETCHFILE'), //Fetch file and file md5 by message md5
|
||||
searchUser ('SEARCHUSR'), //Search username and userid by username
|
||||
addContact ('ADDCONTACT'), //Add one-way relation to a user
|
||||
fetchContact ('FETCHCONTACT'), //Fetch all contacts, including requesting and pending
|
||||
unknown ('UNKNOWN'); //Wrong command
|
||||
|
||||
const RequestType(String value): _value = value;
|
||||
final String _value;
|
||||
String get value => _value;
|
||||
|
||||
//Construct the enum type by value
|
||||
factory RequestType.fromValue(String value) {
|
||||
return RequestType.values.firstWhere((element) => element._value == value, orElse: () => RequestType.unknown);
|
||||
}
|
||||
}
|
||||
|
||||
//Object wrapper for tcp request string
|
||||
class TCPRequest {
|
||||
final Map<String, Object?> _data;
|
||||
File? payload;
|
||||
|
||||
TCPRequest(Uint8List data, this.payload): _data = jsonDecode(String.fromCharCodes(data));
|
||||
|
||||
String get toJSON => jsonEncode(_data);
|
||||
RequestType get requestType => RequestType.fromValue(_data['request'] as String);
|
||||
int? get tokenID => _data['tokenid'] as int?;
|
||||
set tokenID(int? t) => _data['tokenid'] = t;
|
||||
Map<String, Object?> get body => _data['body'] as Map<String, Object?>? ?? {};
|
||||
}
|
||||
53
lib/tcpcontroller/response.dart
Normal file
53
lib/tcpcontroller/response.dart
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-08 22:40:47
|
||||
* @LastEditTime : 2022-10-08 23:05:01
|
||||
* @Description :
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:tcp_server/tcpcontroller/request.dart';
|
||||
|
||||
enum ResponseStatus {
|
||||
ok('OK'),
|
||||
err('ERR');
|
||||
|
||||
final String _value;
|
||||
const ResponseStatus(String v): _value = v;
|
||||
|
||||
String get value => _value;
|
||||
}
|
||||
|
||||
class TCPResponse {
|
||||
final String responseJson;
|
||||
final File? payloadFile;
|
||||
|
||||
TCPResponse({
|
||||
required RequestType type,
|
||||
required ResponseStatus status,
|
||||
Map<String, Object?>? body,
|
||||
String? errInfo,
|
||||
File? payload
|
||||
}):
|
||||
responseJson = jsonEncode({
|
||||
"response": type.value,
|
||||
"status": status.value,
|
||||
"info": errInfo,
|
||||
"body": body,
|
||||
}),
|
||||
payloadFile = payload;
|
||||
|
||||
int get responseLength => responseJson.length;
|
||||
int get payloadLength => payloadFile?.lengthSync() ?? 0;
|
||||
Stream<Uint8List> get stream async* {
|
||||
yield Uint8List(4)..buffer.asInt32List()[0] = responseLength;
|
||||
yield Uint8List(4)..buffer.asInt32List()[0] = payloadLength;
|
||||
yield Uint8List.fromList(responseJson.codeUnits);
|
||||
if(payloadFile != null) {
|
||||
yield await payloadFile!.readAsBytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
22
lib/utils/typeconverter.dart
Normal file
22
lib/utils/typeconverter.dart
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-08 17:21:45
|
||||
* @LastEditTime : 2022-10-08 17:29:21
|
||||
* @Description : Type Converters
|
||||
*/
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:convert/convert.dart';
|
||||
|
||||
String uint8ListToHexString(Uint8List data) {
|
||||
return data.buffer.asUint8List().map((e) => e.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
Uint8List hexToUint8List(String string) {
|
||||
return Uint8List.fromList(hex.decode(string));
|
||||
}
|
||||
|
||||
Uint8List intToUint8List(int value) {
|
||||
return Uint8List(4)..buffer.asInt32List()[0] = value;
|
||||
}
|
||||
369
pubspec.lock
Normal file
369
pubspec.lock
Normal file
@ -0,0 +1,369 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "49.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
convert:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.6.1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.4"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.12.12"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.10.10"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
sqflite_common:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite_common
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
sqflite_common_ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite_common_ffi
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1+1"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0+3"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.21.6"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.4.14"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.4.18"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
udp:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: udp
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.4.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
sdks:
|
||||
dart: ">=2.18.2 <3.0.0"
|
||||
21
pubspec.yaml
Normal file
21
pubspec.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
name: tcp_server
|
||||
description: A sample command-line application.
|
||||
version: 1.0.0
|
||||
# homepage: https://www.example.com
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.2 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
udp: ^5.0.3
|
||||
sqflite_common_ffi: ^2.1.1+1
|
||||
sqflite_common: ^2.3.0
|
||||
crypto: ^3.0.2
|
||||
convert: ^3.0.2
|
||||
|
||||
# dependencies:
|
||||
# path: ^1.8.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.0.0
|
||||
test: ^1.16.0
|
||||
Loading…
x
Reference in New Issue
Block a user