Implement loading meilisearch config from disk

Encrypted such as it is
This commit is contained in:
2025-05-24 01:35:09 +02:00
parent 716a02a1dc
commit fadd9a7387
5 changed files with 201 additions and 31 deletions

View File

@@ -15,6 +15,10 @@ Journaler helps you build a consistent journaling habit by periodically remindin
![Journaler Screenshot](docs/screenshots/journaler_main.png) ![Journaler Screenshot](docs/screenshots/journaler_main.png)
## Note
**Versions up to 3 use a local sqlite database while versions 4 and above use meillisearch!**
## Features ## Features
- **Automated Reminders**: Customizable popup intervals to remind you to journal - **Automated Reminders**: Customizable popup intervals to remind you to journal

View File

@@ -3,22 +3,25 @@ import 'dart:core';
import 'dart:math'; import 'dart:math';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:journaler/notes.dart'; import 'package:journaler/notes.dart';
import 'package:journaler/meilisearch_config.dart';
const endpoint = 'https://meili.site.quack-lab.dev/';
const noteIndex = 'notes'; const noteIndex = 'notes';
const scratchIndex = 'scratch'; const scratchIndex = 'scratch';
const settingsIndex = 'settings'; const settingsIndex = 'settings';
const apiKey = String.fromEnvironment(
'MEILISEARCH_API_KEY',
defaultValue:
'',
);
const header = {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
};
final alphanum = RegExp(r'[a-zA-Z0-9]', caseSensitive: false); final alphanum = RegExp(r'[a-zA-Z0-9]', caseSensitive: false);
Future<Map<String, String>> _getHeaders() async {
final apiKey = await getMeilisearchApiKey();
return {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
};
}
Future<String> _getEndpoint() async {
return await getMeilisearchEndpoint();
}
class MeilisearchQuery { class MeilisearchQuery {
String q; String q;
String? filter; String? filter;
@@ -93,65 +96,72 @@ class MeilisearchResponse {
} }
Future<void> init() async { Future<void> init() async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
if (!await indexExists(noteIndex)) { if (!await indexExists(noteIndex)) {
await http.post( await http.post(
Uri.parse('$endpoint/indexes'), Uri.parse('$endpoint/indexes'),
headers: header, headers: headers,
body: jsonEncode({'uid': noteIndex, 'primaryKey': 'id'}), body: jsonEncode({'uid': noteIndex, 'primaryKey': 'id'}),
); );
} }
await http.put( await http.put(
Uri.parse('$endpoint/indexes/$noteIndex/settings/sortable-attributes'), Uri.parse('$endpoint/indexes/$noteIndex/settings/sortable-attributes'),
headers: header, headers: headers,
body: jsonEncode(['date']), body: jsonEncode(['date']),
); );
await http.put( await http.put(
Uri.parse('$endpoint/indexes/$noteIndex/settings/filterable-attributes'), Uri.parse('$endpoint/indexes/$noteIndex/settings/filterable-attributes'),
headers: header, headers: headers,
body: jsonEncode(['date', 'topLetter', 'topLetterFrequency']), body: jsonEncode(['date', 'topLetter', 'topLetterFrequency']),
); );
if (!await indexExists(scratchIndex)) { if (!await indexExists(scratchIndex)) {
await http.post( await http.post(
Uri.parse('$endpoint/indexes'), Uri.parse('$endpoint/indexes'),
headers: header, headers: headers,
body: jsonEncode({'uid': scratchIndex, 'primaryKey': 'id'}), body: jsonEncode({'uid': scratchIndex, 'primaryKey': 'id'}),
); );
} }
await http.put( await http.put(
Uri.parse('$endpoint/indexes/$scratchIndex/settings/sortable-attributes'), Uri.parse('$endpoint/indexes/$scratchIndex/settings/sortable-attributes'),
headers: header, headers: headers,
body: jsonEncode(['date']), body: jsonEncode(['date']),
); );
await http.put( await http.put(
Uri.parse('$endpoint/indexes/$scratchIndex/settings/filterable-attributes'), Uri.parse('$endpoint/indexes/$scratchIndex/settings/filterable-attributes'),
headers: header, headers: headers,
body: jsonEncode(['date']), body: jsonEncode(['date']),
); );
if (!await indexExists(settingsIndex)) { if (!await indexExists(settingsIndex)) {
await http.post( await http.post(
Uri.parse('$endpoint/indexes'), Uri.parse('$endpoint/indexes'),
headers: header, headers: headers,
body: jsonEncode({'uid': settingsIndex, 'primaryKey': 'key'}), body: jsonEncode({'uid': settingsIndex, 'primaryKey': 'key'}),
); );
} }
} }
Future<bool> indexExists(String index) async { Future<bool> indexExists(String index) async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final response = await http.get( final response = await http.get(
Uri.parse('$endpoint/indexes/$index'), Uri.parse('$endpoint/indexes/$index'),
headers: header, headers: headers,
); );
return response.statusCode == 200; return response.statusCode == 200;
} }
// Settings Management // Settings Management
Future<String?> getSetting(String key) async { Future<String?> getSetting(String key) async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final searchCondition = MeilisearchQuery(q: '', filter: 'key = $key'); final searchCondition = MeilisearchQuery(q: '', filter: 'key = $key');
final response = await http.post( final response = await http.post(
Uri.parse('$endpoint/indexes/$settingsIndex/search'), Uri.parse('$endpoint/indexes/$settingsIndex/search'),
headers: header, headers: headers,
body: jsonEncode(searchCondition.toJson()), body: jsonEncode(searchCondition.toJson()),
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
@@ -165,10 +175,12 @@ Future<String?> getSetting(String key) async {
} }
Future<void> setSetting(String key, String value) async { Future<void> setSetting(String key, String value) async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final document = {'key': key, 'value': value}; final document = {'key': key, 'value': value};
final response = await http.post( final response = await http.post(
Uri.parse('$endpoint/indexes/$settingsIndex/documents'), Uri.parse('$endpoint/indexes/$settingsIndex/documents'),
headers: header, headers: headers,
body: jsonEncode(document), body: jsonEncode(document),
); );
if (response.statusCode != 202) { if (response.statusCode != 202) {
@@ -179,6 +191,8 @@ Future<void> setSetting(String key, String value) async {
// Maybe we could factor a lot of this out into a separate function // Maybe we could factor a lot of this out into a separate function
// But we don't care for now... // But we don't care for now...
Future<List<Note>> searchNotes(String query) async { Future<List<Note>> searchNotes(String query) async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final searchCondition = MeilisearchQuery( final searchCondition = MeilisearchQuery(
q: query, q: query,
limit: 10, limit: 10,
@@ -189,7 +203,7 @@ Future<List<Note>> searchNotes(String query) async {
); );
final response = await http.post( final response = await http.post(
Uri.parse('$endpoint/indexes/$noteIndex/search'), Uri.parse('$endpoint/indexes/$noteIndex/search'),
headers: header, headers: headers,
body: jsonEncode(searchCondition.toJson()), body: jsonEncode(searchCondition.toJson()),
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
@@ -209,6 +223,8 @@ Future<List<Note>> searchNotes(String query) async {
} }
Future<Note?> getPreviousTo(int epochTime) async { Future<Note?> getPreviousTo(int epochTime) async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final searchCondition = MeilisearchQuery( final searchCondition = MeilisearchQuery(
q: '', q: '',
filter: 'date < $epochTime', filter: 'date < $epochTime',
@@ -217,7 +233,7 @@ Future<Note?> getPreviousTo(int epochTime) async {
); );
final response = await http.post( final response = await http.post(
Uri.parse('$endpoint/indexes/$noteIndex/search'), Uri.parse('$endpoint/indexes/$noteIndex/search'),
headers: header, headers: headers,
body: jsonEncode(searchCondition.toJson()), body: jsonEncode(searchCondition.toJson()),
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
@@ -237,6 +253,8 @@ Future<Note?> getPreviousTo(int epochTime) async {
} }
Future<Note?> getNextTo(int epochTime) async { Future<Note?> getNextTo(int epochTime) async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final searchCondition = MeilisearchQuery( final searchCondition = MeilisearchQuery(
q: '', q: '',
filter: 'date > $epochTime', filter: 'date > $epochTime',
@@ -245,7 +263,7 @@ Future<Note?> getNextTo(int epochTime) async {
); );
final response = await http.post( final response = await http.post(
Uri.parse('$endpoint/indexes/$noteIndex/search'), Uri.parse('$endpoint/indexes/$noteIndex/search'),
headers: header, headers: headers,
body: jsonEncode(searchCondition.toJson()), body: jsonEncode(searchCondition.toJson()),
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
@@ -265,6 +283,8 @@ Future<Note?> getNextTo(int epochTime) async {
} }
Future<Note?> getLatest() async { Future<Note?> getLatest() async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final searchCondition = MeilisearchQuery( final searchCondition = MeilisearchQuery(
q: '', q: '',
sort: ['date:desc'], sort: ['date:desc'],
@@ -272,7 +292,7 @@ Future<Note?> getLatest() async {
); );
final response = await http.post( final response = await http.post(
Uri.parse('$endpoint/indexes/$noteIndex/search'), Uri.parse('$endpoint/indexes/$noteIndex/search'),
headers: header, headers: headers,
body: jsonEncode(searchCondition.toJson()), body: jsonEncode(searchCondition.toJson()),
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
@@ -303,6 +323,8 @@ String generateRandomString(int length) {
} }
Future<Note> createNote(String content) async { Future<Note> createNote(String content) async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final lines = content.split('\n'); final lines = content.split('\n');
final trimmedLines = <String>[]; final trimmedLines = <String>[];
for (final line in lines) { for (final line in lines) {
@@ -334,7 +356,7 @@ Future<Note> createNote(String content) async {
}; };
final response = await http.post( final response = await http.post(
Uri.parse('$endpoint/indexes/$noteIndex/documents'), Uri.parse('$endpoint/indexes/$noteIndex/documents'),
headers: header, headers: headers,
body: jsonEncode(document), body: jsonEncode(document),
); );
if (response.statusCode != 202) { if (response.statusCode != 202) {
@@ -348,13 +370,15 @@ Future<Note> createNote(String content) async {
} }
Future<List<Note>> getProblematic({double threshold = 0.7}) async { Future<List<Note>> getProblematic({double threshold = 0.7}) async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final searchCondition = MeilisearchQuery( final searchCondition = MeilisearchQuery(
q: '', q: '',
filter: 'topLetterFrequency > $threshold', filter: 'topLetterFrequency > $threshold',
); );
final response = await http.post( final response = await http.post(
Uri.parse('$endpoint/indexes/$noteIndex/search'), Uri.parse('$endpoint/indexes/$noteIndex/search'),
headers: header, headers: headers,
body: jsonEncode(searchCondition.toJson()), body: jsonEncode(searchCondition.toJson()),
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
@@ -380,6 +404,8 @@ Future<List<Note>> getProblematic({double threshold = 0.7}) async {
// TODO: only update if changed // TODO: only update if changed
// How? idk // How? idk
Future<void> updateNote(Note note) async { Future<void> updateNote(Note note) async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final lines = note.content.split('\n'); final lines = note.content.split('\n');
final trimmedLines = <String>[]; final trimmedLines = <String>[];
for (final line in lines) { for (final line in lines) {
@@ -413,7 +439,7 @@ Future<void> updateNote(Note note) async {
final response = await http.post( final response = await http.post(
Uri.parse('$endpoint/indexes/$noteIndex/documents'), Uri.parse('$endpoint/indexes/$noteIndex/documents'),
headers: header, headers: headers,
body: jsonEncode(document), body: jsonEncode(document),
); );
if (response.statusCode != 202) { if (response.statusCode != 202) {
@@ -424,9 +450,11 @@ Future<void> updateNote(Note note) async {
} }
Future<void> deleteNote(String id) async { Future<void> deleteNote(String id) async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final response = await http.delete( final response = await http.delete(
Uri.parse('$endpoint/indexes/$noteIndex/documents/$id'), Uri.parse('$endpoint/indexes/$noteIndex/documents/$id'),
headers: header, headers: headers,
); );
if (response.statusCode != 202) { if (response.statusCode != 202) {
throw Exception( throw Exception(
@@ -436,6 +464,8 @@ Future<void> deleteNote(String id) async {
} }
Future<Scratch?> getLatestScratch() async { Future<Scratch?> getLatestScratch() async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final searchCondition = MeilisearchQuery( final searchCondition = MeilisearchQuery(
q: '', q: '',
sort: ['date:desc'], sort: ['date:desc'],
@@ -443,7 +473,7 @@ Future<Scratch?> getLatestScratch() async {
); );
final response = await http.post( final response = await http.post(
Uri.parse('$endpoint/indexes/$scratchIndex/search'), Uri.parse('$endpoint/indexes/$scratchIndex/search'),
headers: header, headers: headers,
body: jsonEncode(searchCondition.toJson()), body: jsonEncode(searchCondition.toJson()),
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
@@ -463,6 +493,8 @@ Future<Scratch?> getLatestScratch() async {
} }
Future<Scratch> createScratch(String content) async { Future<Scratch> createScratch(String content) async {
final endpoint = await _getEndpoint();
final headers = await _getHeaders();
final document = { final document = {
'id': generateRandomString(32), 'id': generateRandomString(32),
'date': DateTime.now().toUtc().millisecondsSinceEpoch, 'date': DateTime.now().toUtc().millisecondsSinceEpoch,
@@ -470,7 +502,7 @@ Future<Scratch> createScratch(String content) async {
}; };
final response = await http.post( final response = await http.post(
Uri.parse('$endpoint/indexes/$scratchIndex/documents'), Uri.parse('$endpoint/indexes/$scratchIndex/documents'),
headers: header, headers: headers,
body: jsonEncode(document), body: jsonEncode(document),
); );
if (response.statusCode != 202) { if (response.statusCode != 202) {

133
lib/meilisearch_config.dart Normal file
View File

@@ -0,0 +1,133 @@
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:path/path.dart' as path;
const defaultEndpoint = 'http://localhost:7700';
const defaultApiKey = 'masterKey';
// Cache for configuration
String? _cachedEndpoint;
String? _cachedApiKey;
bool _isInitialized = false;
// Get the config file path in the user's home directory
Future<String> _getConfigPath() async {
final home =
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
if (home == null) {
throw Exception('Could not find home directory');
}
final configDir = Directory(path.join(home, '.journaler'));
if (!await configDir.exists()) {
await configDir.create(recursive: true);
}
return path.join(configDir.path, 'meilisearch_config.enc');
}
// Simple encryption key derived from machine-specific data
String _getEncryptionKey() {
final machineId =
Platform.operatingSystem +
Platform.operatingSystemVersion +
Platform.localHostname;
return sha256.convert(utf8.encode(machineId)).toString();
}
// Encrypt data
String _encrypt(String data) {
final key = _getEncryptionKey();
final bytes = utf8.encode(data);
final encrypted = <int>[];
for (var i = 0; i < bytes.length; i++) {
encrypted.add(bytes[i] ^ key.codeUnitAt(i % key.length));
}
return base64.encode(encrypted);
}
// Decrypt data
String _decrypt(String encrypted) {
final key = _getEncryptionKey();
final bytes = base64.decode(encrypted);
final decrypted = <int>[];
for (var i = 0; i < bytes.length; i++) {
decrypted.add(bytes[i] ^ key.codeUnitAt(i % key.length));
}
return utf8.decode(decrypted);
}
// Initialize cache from file
Future<void> _initializeCache() async {
if (_isInitialized) return;
try {
final configPath = await _getConfigPath();
final file = File(configPath);
if (!await file.exists()) {
_cachedEndpoint = defaultEndpoint;
_cachedApiKey = defaultApiKey;
_isInitialized = true;
return;
}
final encrypted = await file.readAsString();
final decrypted = _decrypt(encrypted);
final config = jsonDecode(decrypted);
_cachedEndpoint = config['endpoint'] ?? defaultEndpoint;
_cachedApiKey = config['apiKey'] ?? defaultApiKey;
} catch (e) {
_cachedEndpoint = defaultEndpoint;
_cachedApiKey = defaultApiKey;
}
_isInitialized = true;
}
Future<String> getMeilisearchEndpoint() async {
await _initializeCache();
return _cachedEndpoint!;
}
Future<String> getMeilisearchApiKey() async {
await _initializeCache();
return _cachedApiKey!;
}
Future<void> setMeilisearchEndpoint(String endpoint) async {
final configPath = await _getConfigPath();
final file = File(configPath);
Map<String, dynamic> config = {};
if (await file.exists()) {
final encrypted = await file.readAsString();
final decrypted = _decrypt(encrypted);
config = jsonDecode(decrypted);
}
config['endpoint'] = endpoint;
final encrypted = _encrypt(jsonEncode(config));
await file.writeAsString(encrypted);
// Update cache
_cachedEndpoint = endpoint;
}
Future<void> setMeilisearchApiKey(String apiKey) async {
final configPath = await _getConfigPath();
final file = File(configPath);
Map<String, dynamic> config = {};
if (await file.exists()) {
final encrypted = await file.readAsString();
final decrypted = _decrypt(encrypted);
config = jsonDecode(decrypted);
}
config['apiKey'] = apiKey;
final encrypted = _encrypt(jsonEncode(config));
await file.writeAsString(encrypted);
// Update cache
_cachedApiKey = apiKey;
}

View File

@@ -130,7 +130,7 @@ packages:
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
crypto: crypto:
dependency: transitive dependency: "direct main"
description: description:
name: crypto name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"

View File

@@ -42,6 +42,7 @@ dependencies:
ps_list: ^0.0.5 ps_list: ^0.0.5
intl: ^0.20.2 intl: ^0.20.2
http: ^1.4.0 http: ^1.4.0
crypto: ^3.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: