Simplify application structure and remove obsolete code
b205012 parent: bc6fa20 modified
app/README.md +1 -1 | @@ -1,4 +1,4 @@ | ||
| 1 | -# glean_app | |
| 1 | +# pulseboard_app | |
| 2 | 2 | |
| 3 | 3 | A new Flutter project. |
| 4 | 4 | |
| @@ -1,4 +1,4 @@ | |||
| 1 | -# glean_app | 1 | +# pulseboard_app |
| 2 | 2 | ||
| 3 | A new Flutter project. | 3 | A new Flutter project. |
| 4 | 4 | ||
modified
app/android/app/build.gradle.kts +2 -2 | @@ -5,7 +5,7 @@ plugins { | ||
| 5 | 5 | } |
| 6 | 6 | |
| 7 | 7 | android { |
| 8 | - namespace = "fr.rbrt.glean.glean_app" | |
| 8 | + namespace = "fr.rbrt.pulseboard.pulseboard_app" | |
| 9 | 9 | compileSdk = flutter.compileSdkVersion |
| 10 | 10 | ndkVersion = flutter.ndkVersion |
| 11 | 11 | |
| @@ -16,7 +16,7 @@ android { | ||
| 16 | 16 | |
| 17 | 17 | defaultConfig { |
| 18 | 18 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). |
| 19 | - applicationId = "fr.rbrt.glean.glean_app" | |
| 19 | + applicationId = "fr.rbrt.pulseboard.pulseboard_app" | |
| 20 | 20 | // You can update the following values to match your application needs. |
| 21 | 21 | // For more information, see: https://flutter.dev/to/review-gradle-config. |
| 22 | 22 | minSdk = flutter.minSdkVersion |
| @@ -5,7 +5,7 @@ plugins { | |||
| 5 | } | 5 | } |
| 6 | 6 | ||
| 7 | android { | 7 | android { |
| 8 | - namespace = "fr.rbrt.glean.glean_app" | 8 | + namespace = "fr.rbrt.pulseboard.pulseboard_app" |
| 9 | compileSdk = flutter.compileSdkVersion | 9 | compileSdk = flutter.compileSdkVersion |
| 10 | ndkVersion = flutter.ndkVersion | 10 | ndkVersion = flutter.ndkVersion |
| 11 | 11 | ||
| @@ -16,7 +16,7 @@ android { | |||
| 16 | 16 | ||
| 17 | defaultConfig { | 17 | defaultConfig { |
| 18 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). | 18 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). |
| 19 | - applicationId = "fr.rbrt.glean.glean_app" | 19 | + applicationId = "fr.rbrt.pulseboard.pulseboard_app" |
| 20 | // You can update the following values to match your application needs. | 20 | // You can update the following values to match your application needs. |
| 21 | // For more information, see: https://flutter.dev/to/review-gradle-config. | 21 | // For more information, see: https://flutter.dev/to/review-gradle-config. |
| 22 | minSdk = flutter.minSdkVersion | 22 | minSdk = flutter.minSdkVersion |
modified
app/android/app/src/main/AndroidManifest.xml +1 -1 | @@ -1,6 +1,6 @@ | ||
| 1 | 1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
| 2 | 2 | <application |
| 3 | - android:label="glean_app" | |
| 3 | + android:label="pulseboard_app" | |
| 4 | 4 | android:name="${applicationName}" |
| 5 | 5 | android:icon="@mipmap/ic_launcher"> |
| 6 | 6 | <activity |
| @@ -1,6 +1,6 @@ | |||
| 1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | 1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
| 2 | <application | 2 | <application |
| 3 | - android:label="glean_app" | 3 | + android:label="pulseboard_app" |
| 4 | android:name="${applicationName}" | 4 | android:name="${applicationName}" |
| 5 | android:icon="@mipmap/ic_launcher"> | 5 | android:icon="@mipmap/ic_launcher"> |
| 6 | <activity | 6 | <activity |
renamed
app/android/app/src/main/kotlin/fr/rbrt/pulseboard/pulseboard_app/MainActivity.kt +1 -1 | similarity index 68% | ||
| rename from app/android/app/src/main/kotlin/fr/rbrt/glean/glean_app/MainActivity.kt | ||
| rename to app/android/app/src/main/kotlin/fr/rbrt/pulseboard/pulseboard_app/MainActivity.kt | ||
| @@ -1,4 +1,4 @@ | ||
| 1 | -package fr.rbrt.glean.glean_app | |
| 1 | +package fr.rbrt.pulseboard.pulseboard_app | |
| 2 | 2 | |
| 3 | 3 | import io.flutter.embedding.android.FlutterActivity |
| 4 | 4 | |
| similarity index 68% | |||
| rename from app/android/app/src/main/kotlin/fr/rbrt/glean/glean_app/MainActivity.kt | |||
| rename to app/android/app/src/main/kotlin/fr/rbrt/pulseboard/pulseboard_app/MainActivity.kt | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | -package fr.rbrt.glean.glean_app | 1 | +package fr.rbrt.pulseboard.pulseboard_app |
| 2 | 2 | ||
| 3 | import io.flutter.embedding.android.FlutterActivity | 3 | import io.flutter.embedding.android.FlutterActivity |
| 4 | 4 | ||
modified
app/lib/main.dart +2 -2 | @@ -38,8 +38,8 @@ class _PulseboardAppState extends State<PulseboardApp> { | ||
| 38 | 38 | child: MaterialApp( |
| 39 | 39 | title: 'Pulseboard', |
| 40 | 40 | debugShowCheckedModeBanner: false, |
| 41 | - theme: gleanTheme(Brightness.light), | |
| 42 | - darkTheme: gleanTheme(Brightness.dark), | |
| 41 | + theme: pulseboardTheme(Brightness.light), | |
| 42 | + darkTheme: pulseboardTheme(Brightness.dark), | |
| 43 | 43 | home: const HomeShell(), |
| 44 | 44 | ), |
| 45 | 45 | ); |
| @@ -38,8 +38,8 @@ class _PulseboardAppState extends State<PulseboardApp> { | |||
| 38 | child: MaterialApp( | 38 | child: MaterialApp( |
| 39 | title: 'Pulseboard', | 39 | title: 'Pulseboard', |
| 40 | debugShowCheckedModeBanner: false, | 40 | debugShowCheckedModeBanner: false, |
| 41 | - theme: gleanTheme(Brightness.light), | 41 | + theme: pulseboardTheme(Brightness.light), |
| 42 | - darkTheme: gleanTheme(Brightness.dark), | 42 | + darkTheme: pulseboardTheme(Brightness.dark), |
| 43 | home: const HomeShell(), | 43 | home: const HomeShell(), |
| 44 | ), | 44 | ), |
| 45 | ); | 45 | ); |
modified
app/lib/src/api/client.dart +4 -4 | @@ -16,13 +16,13 @@ class ApiException implements Exception { | ||
| 16 | 16 | /// |
| 17 | 17 | /// No cookies, no CSRF, no auth: the server is single-user and binds |
| 18 | 18 | /// loopback. That is why this class is a thin wrapper rather than a session. |
| 19 | -class GleanClient { | |
| 20 | - GleanClient({required this.baseUrl, this.token = '', http.Client? client}) | |
| 19 | +class PulseboardClient { | |
| 20 | + PulseboardClient({required this.baseUrl, this.token = '', http.Client? client}) | |
| 21 | 21 | : _client = client ?? http.Client(); |
| 22 | 22 | |
| 23 | 23 | final String baseUrl; |
| 24 | 24 | |
| 25 | - /// Shared secret, when the reader is running with GLEAN_TOKEN set. Empty | |
| 25 | + /// Shared secret, when the reader is running with PULSEBOARD_TOKEN set. Empty | |
| 26 | 26 | /// for a local reader, which has no gate. |
| 27 | 27 | /// |
| 28 | 28 | /// Sent as a header rather than a query parameter so it stays out of the |
| @@ -32,7 +32,7 @@ class GleanClient { | ||
| 32 | 32 | final http.Client _client; |
| 33 | 33 | |
| 34 | 34 | Map<String, String> get _headers => |
| 35 | - token.isEmpty ? const {} : {'X-Glean-Token': token}; | |
| 35 | + token.isEmpty ? const {} : {'X-Pulseboard-Token': token}; | |
| 36 | 36 | |
| 37 | 37 | Uri _uri(String path, [Map<String, String>? query]) { |
| 38 | 38 | final q = query?.entries.where((e) => e.value.isNotEmpty); |
| @@ -16,13 +16,13 @@ class ApiException implements Exception { | |||
| 16 | /// | 16 | /// |
| 17 | /// No cookies, no CSRF, no auth: the server is single-user and binds | 17 | /// No cookies, no CSRF, no auth: the server is single-user and binds |
| 18 | /// loopback. That is why this class is a thin wrapper rather than a session. | 18 | /// loopback. That is why this class is a thin wrapper rather than a session. |
| 19 | -class GleanClient { | 19 | +class PulseboardClient { |
| 20 | - GleanClient({required this.baseUrl, this.token = '', http.Client? client}) | 20 | + PulseboardClient({required this.baseUrl, this.token = '', http.Client? client}) |
| 21 | : _client = client ?? http.Client(); | 21 | : _client = client ?? http.Client(); |
| 22 | 22 | ||
| 23 | final String baseUrl; | 23 | final String baseUrl; |
| 24 | 24 | ||
| 25 | - /// Shared secret, when the reader is running with GLEAN_TOKEN set. Empty | 25 | + /// Shared secret, when the reader is running with PULSEBOARD_TOKEN set. Empty |
| 26 | /// for a local reader, which has no gate. | 26 | /// for a local reader, which has no gate. |
| 27 | /// | 27 | /// |
| 28 | /// Sent as a header rather than a query parameter so it stays out of the | 28 | /// Sent as a header rather than a query parameter so it stays out of the |
| @@ -32,7 +32,7 @@ class GleanClient { | |||
| 32 | final http.Client _client; | 32 | final http.Client _client; |
| 33 | 33 | ||
| 34 | Map<String, String> get _headers => | 34 | Map<String, String> get _headers => |
| 35 | - token.isEmpty ? const {} : {'X-Glean-Token': token}; | 35 | + token.isEmpty ? const {} : {'X-Pulseboard-Token': token}; |
| 36 | 36 | ||
| 37 | Uri _uri(String path, [Map<String, String>? query]) { | 37 | Uri _uri(String path, [Map<String, String>? query]) { |
| 38 | final q = query?.entries.where((e) => e.value.isNotEmpty); | 38 | final q = query?.entries.where((e) => e.value.isNotEmpty); |
modified
app/lib/src/app_state.dart +7 -7 | @@ -5,7 +5,7 @@ import 'api/client.dart'; | ||
| 5 | 5 | import 'models/models.dart'; |
| 6 | 6 | |
| 7 | 7 | /// Where the reader is. Override at build time: |
| 8 | -/// flutter run --dart-define=GLEAN_BASE_URL=http://10.0.2.2:8080 | |
| 8 | +/// flutter run --dart-define=PULSEBOARD_BASE_URL=http://10.0.2.2:8080 | |
| 9 | 9 | /// |
| 10 | 10 | /// 10.0.2.2 is how the Android emulator reaches the host's loopback; a real |
| 11 | 11 | /// device needs the machine's LAN address. |
| @@ -14,7 +14,7 @@ import 'models/models.dart'; | ||
| 14 | 14 | /// relative paths: same origin, which means no CORS preflight and the |
| 15 | 15 | /// reader's own cookie on every request. |
| 16 | 16 | const kConfiguredBaseUrl = String.fromEnvironment( |
| 17 | - 'GLEAN_BASE_URL', | |
| 17 | + 'PULSEBOARD_BASE_URL', | |
| 18 | 18 | defaultValue: '', |
| 19 | 19 | ); |
| 20 | 20 | |
| @@ -22,12 +22,12 @@ String get kDefaultBaseUrl => kConfiguredBaseUrl.isNotEmpty | ||
| 22 | 22 | ? kConfiguredBaseUrl |
| 23 | 23 | : (kIsWeb ? '' : 'http://127.0.0.1:8080'); |
| 24 | 24 | |
| 25 | -/// Shared secret, for a reader running with GLEAN_TOKEN set. | |
| 25 | +/// Shared secret, for a reader running with PULSEBOARD_TOKEN set. | |
| 26 | 26 | /// |
| 27 | 27 | /// Empty by default, which is right for a local reader and for the web build |
| 28 | 28 | /// served by the reader itself -- there the browser already holds the cookie |
| 29 | 29 | /// the reader set, and same-origin requests carry it automatically. |
| 30 | -const kToken = String.fromEnvironment('GLEAN_TOKEN', defaultValue: ''); | |
| 30 | +const kToken = String.fromEnvironment('PULSEBOARD_TOKEN', defaultValue: ''); | |
| 31 | 31 | |
| 32 | 32 | /// Feed list and unread counts, shared across screens. |
| 33 | 33 | /// |
| @@ -38,10 +38,10 @@ class AppState extends ChangeNotifier { | ||
| 38 | 38 | AppState({ |
| 39 | 39 | String? baseUrl, |
| 40 | 40 | String token = kToken, |
| 41 | - GleanClient? client, | |
| 42 | - }) : client = client ?? GleanClient(baseUrl: baseUrl ?? kDefaultBaseUrl, token: token); | |
| 41 | + PulseboardClient? client, | |
| 42 | + }) : client = client ?? PulseboardClient(baseUrl: baseUrl ?? kDefaultBaseUrl, token: token); | |
| 43 | 43 | |
| 44 | - final GleanClient client; | |
| 44 | + final PulseboardClient client; | |
| 45 | 45 | |
| 46 | 46 | List<Feed> _feeds = const []; |
| 47 | 47 | int _unread = 0; |
| @@ -5,7 +5,7 @@ import 'api/client.dart'; | |||
| 5 | import 'models/models.dart'; | 5 | import 'models/models.dart'; |
| 6 | 6 | ||
| 7 | /// Where the reader is. Override at build time: | 7 | /// Where the reader is. Override at build time: |
| 8 | -/// flutter run --dart-define=GLEAN_BASE_URL=http://10.0.2.2:8080 | 8 | +/// flutter run --dart-define=PULSEBOARD_BASE_URL=http://10.0.2.2:8080 |
| 9 | /// | 9 | /// |
| 10 | /// 10.0.2.2 is how the Android emulator reaches the host's loopback; a real | 10 | /// 10.0.2.2 is how the Android emulator reaches the host's loopback; a real |
| 11 | /// device needs the machine's LAN address. | 11 | /// device needs the machine's LAN address. |
| @@ -14,7 +14,7 @@ import 'models/models.dart'; | |||
| 14 | /// relative paths: same origin, which means no CORS preflight and the | 14 | /// relative paths: same origin, which means no CORS preflight and the |
| 15 | /// reader's own cookie on every request. | 15 | /// reader's own cookie on every request. |
| 16 | const kConfiguredBaseUrl = String.fromEnvironment( | 16 | const kConfiguredBaseUrl = String.fromEnvironment( |
| 17 | - 'GLEAN_BASE_URL', | 17 | + 'PULSEBOARD_BASE_URL', |
| 18 | defaultValue: '', | 18 | defaultValue: '', |
| 19 | ); | 19 | ); |
| 20 | 20 | ||
| @@ -22,12 +22,12 @@ String get kDefaultBaseUrl => kConfiguredBaseUrl.isNotEmpty | |||
| 22 | ? kConfiguredBaseUrl | 22 | ? kConfiguredBaseUrl |
| 23 | : (kIsWeb ? '' : 'http://127.0.0.1:8080'); | 23 | : (kIsWeb ? '' : 'http://127.0.0.1:8080'); |
| 24 | 24 | ||
| 25 | -/// Shared secret, for a reader running with GLEAN_TOKEN set. | 25 | +/// Shared secret, for a reader running with PULSEBOARD_TOKEN set. |
| 26 | /// | 26 | /// |
| 27 | /// Empty by default, which is right for a local reader and for the web build | 27 | /// Empty by default, which is right for a local reader and for the web build |
| 28 | /// served by the reader itself -- there the browser already holds the cookie | 28 | /// served by the reader itself -- there the browser already holds the cookie |
| 29 | /// the reader set, and same-origin requests carry it automatically. | 29 | /// the reader set, and same-origin requests carry it automatically. |
| 30 | -const kToken = String.fromEnvironment('GLEAN_TOKEN', defaultValue: ''); | 30 | +const kToken = String.fromEnvironment('PULSEBOARD_TOKEN', defaultValue: ''); |
| 31 | 31 | ||
| 32 | /// Feed list and unread counts, shared across screens. | 32 | /// Feed list and unread counts, shared across screens. |
| 33 | /// | 33 | /// |
| @@ -38,10 +38,10 @@ class AppState extends ChangeNotifier { | |||
| 38 | AppState({ | 38 | AppState({ |
| 39 | String? baseUrl, | 39 | String? baseUrl, |
| 40 | String token = kToken, | 40 | String token = kToken, |
| 41 | - GleanClient? client, | 41 | + PulseboardClient? client, |
| 42 | - }) : client = client ?? GleanClient(baseUrl: baseUrl ?? kDefaultBaseUrl, token: token); | 42 | + }) : client = client ?? PulseboardClient(baseUrl: baseUrl ?? kDefaultBaseUrl, token: token); |
| 43 | 43 | ||
| 44 | - final GleanClient client; | 44 | + final PulseboardClient client; |
| 45 | 45 | ||
| 46 | List<Feed> _feeds = const []; | 46 | List<Feed> _feeds = const []; |
| 47 | int _unread = 0; | 47 | int _unread = 0; |
modified
app/lib/src/screens/articles_screen.dart +1 -1 | @@ -172,7 +172,7 @@ class _StatusFilter extends StatelessWidget { | ||
| 172 | 172 | |
| 173 | 173 | @override |
| 174 | 174 | Widget build(BuildContext context) { |
| 175 | - final c = GleanColors.of(context); | |
| 175 | + final c = PulseboardColors.of(context); | |
| 176 | 176 | return DropdownButton<String>( |
| 177 | 177 | value: status, |
| 178 | 178 | underline: const SizedBox.shrink(), |
| @@ -172,7 +172,7 @@ class _StatusFilter extends StatelessWidget { | |||
| 172 | 172 | ||
| 173 | @override | 173 | @override |
| 174 | Widget build(BuildContext context) { | 174 | Widget build(BuildContext context) { |
| 175 | - final c = GleanColors.of(context); | 175 | + final c = PulseboardColors.of(context); |
| 176 | return DropdownButton<String>( | 176 | return DropdownButton<String>( |
| 177 | value: status, | 177 | value: status, |
| 178 | underline: const SizedBox.shrink(), | 178 | underline: const SizedBox.shrink(), |
modified
app/lib/src/screens/feed_manager_screen.dart +1 -1 | @@ -108,7 +108,7 @@ class _FeedManagerScreenState extends State<FeedManagerScreen> { | ||
| 108 | 108 | @override |
| 109 | 109 | Widget build(BuildContext context) { |
| 110 | 110 | final app = AppScope.of(context); |
| 111 | - final c = GleanColors.of(context); | |
| 111 | + final c = PulseboardColors.of(context); | |
| 112 | 112 | final filter = _filter.text.trim().toLowerCase(); |
| 113 | 113 | final feeds = app.feeds.where((feed) { |
| 114 | 114 | return filter.isEmpty || |
| @@ -108,7 +108,7 @@ class _FeedManagerScreenState extends State<FeedManagerScreen> { | |||
| 108 | @override | 108 | @override |
| 109 | Widget build(BuildContext context) { | 109 | Widget build(BuildContext context) { |
| 110 | final app = AppScope.of(context); | 110 | final app = AppScope.of(context); |
| 111 | - final c = GleanColors.of(context); | 111 | + final c = PulseboardColors.of(context); |
| 112 | final filter = _filter.text.trim().toLowerCase(); | 112 | final filter = _filter.text.trim().toLowerCase(); |
| 113 | final feeds = app.feeds.where((feed) { | 113 | final feeds = app.feeds.where((feed) { |
| 114 | return filter.isEmpty || | 114 | return filter.isEmpty || |
modified
app/lib/src/screens/home_shell.dart +3 -3 | @@ -96,11 +96,11 @@ class _HomeShellState extends State<HomeShell> { | ||
| 96 | 96 | return FeedManagerScreen(onClose: _closeManageFeeds); |
| 97 | 97 | } |
| 98 | 98 | final app = AppScope.of(context); |
| 99 | - final c = GleanColors.of(context); | |
| 99 | + final c = PulseboardColors.of(context); | |
| 100 | 100 | |
| 101 | 101 | return Scaffold( |
| 102 | 102 | appBar: AppBar( |
| 103 | - title: Text(_selected.isAll ? 'glean' : _selected.title), | |
| 103 | + title: Text(_selected.isAll ? 'Pulseboard' : _selected.title), | |
| 104 | 104 | actions: [ |
| 105 | 105 | IconButton( |
| 106 | 106 | tooltip: 'Manage feeds', |
| @@ -170,7 +170,7 @@ class _HomeShellState extends State<HomeShell> { | ||
| 170 | 170 | overflow: TextOverflow.ellipsis, |
| 171 | 171 | ), |
| 172 | 172 | trailing: f.unread > 0 |
| 173 | - ? GleanTag('${f.unread}', emphasis: !f.isAll) | |
| 173 | + ? PulseboardTag('${f.unread}', emphasis: !f.isAll) | |
| 174 | 174 | : null, |
| 175 | 175 | onTap: () { |
| 176 | 176 | setState(() => _selected = f); |
| @@ -96,11 +96,11 @@ class _HomeShellState extends State<HomeShell> { | |||
| 96 | return FeedManagerScreen(onClose: _closeManageFeeds); | 96 | return FeedManagerScreen(onClose: _closeManageFeeds); |
| 97 | } | 97 | } |
| 98 | final app = AppScope.of(context); | 98 | final app = AppScope.of(context); |
| 99 | - final c = GleanColors.of(context); | 99 | + final c = PulseboardColors.of(context); |
| 100 | 100 | ||
| 101 | return Scaffold( | 101 | return Scaffold( |
| 102 | appBar: AppBar( | 102 | appBar: AppBar( |
| 103 | - title: Text(_selected.isAll ? 'glean' : _selected.title), | 103 | + title: Text(_selected.isAll ? 'Pulseboard' : _selected.title), |
| 104 | actions: [ | 104 | actions: [ |
| 105 | IconButton( | 105 | IconButton( |
| 106 | tooltip: 'Manage feeds', | 106 | tooltip: 'Manage feeds', |
| @@ -170,7 +170,7 @@ class _HomeShellState extends State<HomeShell> { | |||
| 170 | overflow: TextOverflow.ellipsis, | 170 | overflow: TextOverflow.ellipsis, |
| 171 | ), | 171 | ), |
| 172 | trailing: f.unread > 0 | 172 | trailing: f.unread > 0 |
| 173 | - ? GleanTag('${f.unread}', emphasis: !f.isAll) | 173 | + ? PulseboardTag('${f.unread}', emphasis: !f.isAll) |
| 174 | : null, | 174 | : null, |
| 175 | onTap: () { | 175 | onTap: () { |
| 176 | setState(() => _selected = f); | 176 | setState(() => _selected = f); |
modified
app/lib/src/theme.dart +12 -12 | @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; | ||
| 2 | 2 | |
| 3 | 3 | /// Flutter port of web/src/app.css. |
| 4 | 4 | /// |
| 5 | -/// Glean's web design system is deliberately brutalist: a monochrome ink/paper | |
| 5 | +/// Pulseboard's web design system is deliberately brutalist: a monochrome ink/paper | |
| 6 | 6 | /// base with one accent green, sharp corners, 2px borders and hard offset |
| 7 | 7 | /// shadows, monospace throughout. Keeping that identity matters more than |
| 8 | 8 | /// looking like stock Material, so this leans on a shared token set rather |
| 9 | 9 | /// than ColorScheme.fromSeed. |
| 10 | -class GleanColors extends ThemeExtension<GleanColors> { | |
| 11 | - const GleanColors({ | |
| 10 | +class PulseboardColors extends ThemeExtension<PulseboardColors> { | |
| 11 | + const PulseboardColors({ | |
| 12 | 12 | required this.bg, |
| 13 | 13 | required this.fg, |
| 14 | 14 | required this.surface, |
| @@ -30,7 +30,7 @@ class GleanColors extends ThemeExtension<GleanColors> { | ||
| 30 | 30 | final Color accentInk; |
| 31 | 31 | final Color danger; |
| 32 | 32 | |
| 33 | - static const light = GleanColors( | |
| 33 | + static const light = PulseboardColors( | |
| 34 | 34 | bg: Color(0xFFFAFAF7), |
| 35 | 35 | fg: Color(0xFF0A0A0A), |
| 36 | 36 | surface: Color(0xFFF0EFE9), |
| @@ -42,7 +42,7 @@ class GleanColors extends ThemeExtension<GleanColors> { | ||
| 42 | 42 | danger: Color(0xFFC82014), |
| 43 | 43 | ); |
| 44 | 44 | |
| 45 | - static const dark = GleanColors( | |
| 45 | + static const dark = PulseboardColors( | |
| 46 | 46 | bg: Color(0xFF0A0A0A), |
| 47 | 47 | fg: Color(0xFFF5F5EF), |
| 48 | 48 | surface: Color(0xFF161616), |
| @@ -54,15 +54,15 @@ class GleanColors extends ThemeExtension<GleanColors> { | ||
| 54 | 54 | danger: Color(0xFFFF5A4D), |
| 55 | 55 | ); |
| 56 | 56 | |
| 57 | - static GleanColors of(BuildContext context) => | |
| 58 | - Theme.of(context).extension<GleanColors>()!; | |
| 57 | + static PulseboardColors of(BuildContext context) => | |
| 58 | + Theme.of(context).extension<PulseboardColors>()!; | |
| 59 | 59 | |
| 60 | 60 | @override |
| 61 | - GleanColors copyWith() => this; | |
| 61 | + PulseboardColors copyWith() => this; | |
| 62 | 62 | |
| 63 | 63 | @override |
| 64 | - GleanColors lerp(ThemeExtension<GleanColors>? other, double t) => | |
| 65 | - t < 0.5 ? this : (other as GleanColors? ?? this); | |
| 64 | + PulseboardColors lerp(ThemeExtension<PulseboardColors>? other, double t) => | |
| 65 | + t < 0.5 ? this : (other as PulseboardColors? ?? this); | |
| 66 | 66 | } |
| 67 | 67 | |
| 68 | 68 | /// The web app asks for JetBrains Mono and falls back through IBM Plex Mono to |
| @@ -70,8 +70,8 @@ class GleanColors extends ThemeExtension<GleanColors> { | ||
| 70 | 70 | /// runtime font download; bundling the TTF would pin it exactly. |
| 71 | 71 | const kMonoFallback = <String>['JetBrains Mono', 'IBM Plex Mono', 'monospace']; |
| 72 | 72 | |
| 73 | -ThemeData gleanTheme(Brightness brightness) { | |
| 74 | - final c = brightness == Brightness.dark ? GleanColors.dark : GleanColors.light; | |
| 73 | +ThemeData pulseboardTheme(Brightness brightness) { | |
| 74 | + final c = brightness == Brightness.dark ? PulseboardColors.dark : PulseboardColors.light; | |
| 75 | 75 | |
| 76 | 76 | TextStyle mono(double size, {FontWeight weight = FontWeight.w400, Color? color}) => |
| 77 | 77 | TextStyle( |
| @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; | |||
| 2 | 2 | ||
| 3 | /// Flutter port of web/src/app.css. | 3 | /// Flutter port of web/src/app.css. |
| 4 | /// | 4 | /// |
| 5 | -/// Glean's web design system is deliberately brutalist: a monochrome ink/paper | 5 | +/// Pulseboard's web design system is deliberately brutalist: a monochrome ink/paper |
| 6 | /// base with one accent green, sharp corners, 2px borders and hard offset | 6 | /// base with one accent green, sharp corners, 2px borders and hard offset |
| 7 | /// shadows, monospace throughout. Keeping that identity matters more than | 7 | /// shadows, monospace throughout. Keeping that identity matters more than |
| 8 | /// looking like stock Material, so this leans on a shared token set rather | 8 | /// looking like stock Material, so this leans on a shared token set rather |
| 9 | /// than ColorScheme.fromSeed. | 9 | /// than ColorScheme.fromSeed. |
| 10 | -class GleanColors extends ThemeExtension<GleanColors> { | 10 | +class PulseboardColors extends ThemeExtension<PulseboardColors> { |
| 11 | - const GleanColors({ | 11 | + const PulseboardColors({ |
| 12 | required this.bg, | 12 | required this.bg, |
| 13 | required this.fg, | 13 | required this.fg, |
| 14 | required this.surface, | 14 | required this.surface, |
| @@ -30,7 +30,7 @@ class GleanColors extends ThemeExtension<GleanColors> { | |||
| 30 | final Color accentInk; | 30 | final Color accentInk; |
| 31 | final Color danger; | 31 | final Color danger; |
| 32 | 32 | ||
| 33 | - static const light = GleanColors( | 33 | + static const light = PulseboardColors( |
| 34 | bg: Color(0xFFFAFAF7), | 34 | bg: Color(0xFFFAFAF7), |
| 35 | fg: Color(0xFF0A0A0A), | 35 | fg: Color(0xFF0A0A0A), |
| 36 | surface: Color(0xFFF0EFE9), | 36 | surface: Color(0xFFF0EFE9), |
| @@ -42,7 +42,7 @@ class GleanColors extends ThemeExtension<GleanColors> { | |||
| 42 | danger: Color(0xFFC82014), | 42 | danger: Color(0xFFC82014), |
| 43 | ); | 43 | ); |
| 44 | 44 | ||
| 45 | - static const dark = GleanColors( | 45 | + static const dark = PulseboardColors( |
| 46 | bg: Color(0xFF0A0A0A), | 46 | bg: Color(0xFF0A0A0A), |
| 47 | fg: Color(0xFFF5F5EF), | 47 | fg: Color(0xFFF5F5EF), |
| 48 | surface: Color(0xFF161616), | 48 | surface: Color(0xFF161616), |
| @@ -54,15 +54,15 @@ class GleanColors extends ThemeExtension<GleanColors> { | |||
| 54 | danger: Color(0xFFFF5A4D), | 54 | danger: Color(0xFFFF5A4D), |
| 55 | ); | 55 | ); |
| 56 | 56 | ||
| 57 | - static GleanColors of(BuildContext context) => | 57 | + static PulseboardColors of(BuildContext context) => |
| 58 | - Theme.of(context).extension<GleanColors>()!; | 58 | + Theme.of(context).extension<PulseboardColors>()!; |
| 59 | 59 | ||
| 60 | @override | 60 | @override |
| 61 | - GleanColors copyWith() => this; | 61 | + PulseboardColors copyWith() => this; |
| 62 | 62 | ||
| 63 | @override | 63 | @override |
| 64 | - GleanColors lerp(ThemeExtension<GleanColors>? other, double t) => | 64 | + PulseboardColors lerp(ThemeExtension<PulseboardColors>? other, double t) => |
| 65 | - t < 0.5 ? this : (other as GleanColors? ?? this); | 65 | + t < 0.5 ? this : (other as PulseboardColors? ?? this); |
| 66 | } | 66 | } |
| 67 | 67 | ||
| 68 | /// The web app asks for JetBrains Mono and falls back through IBM Plex Mono to | 68 | /// The web app asks for JetBrains Mono and falls back through IBM Plex Mono to |
| @@ -70,8 +70,8 @@ class GleanColors extends ThemeExtension<GleanColors> { | |||
| 70 | /// runtime font download; bundling the TTF would pin it exactly. | 70 | /// runtime font download; bundling the TTF would pin it exactly. |
| 71 | const kMonoFallback = <String>['JetBrains Mono', 'IBM Plex Mono', 'monospace']; | 71 | const kMonoFallback = <String>['JetBrains Mono', 'IBM Plex Mono', 'monospace']; |
| 72 | 72 | ||
| 73 | -ThemeData gleanTheme(Brightness brightness) { | 73 | +ThemeData pulseboardTheme(Brightness brightness) { |
| 74 | - final c = brightness == Brightness.dark ? GleanColors.dark : GleanColors.light; | 74 | + final c = brightness == Brightness.dark ? PulseboardColors.dark : PulseboardColors.light; |
| 75 | 75 | ||
| 76 | TextStyle mono(double size, {FontWeight weight = FontWeight.w400, Color? color}) => | 76 | TextStyle mono(double size, {FontWeight weight = FontWeight.w400, Color? color}) => |
| 77 | TextStyle( | 77 | TextStyle( |
modified
app/lib/src/widgets/article_tile.dart +1 -1 | @@ -20,7 +20,7 @@ class ArticleTile extends StatelessWidget { | ||
| 20 | 20 | |
| 21 | 21 | @override |
| 22 | 22 | Widget build(BuildContext context) { |
| 23 | - final c = GleanColors.of(context); | |
| 23 | + final c = PulseboardColors.of(context); | |
| 24 | 24 | final text = Theme.of(context).textTheme; |
| 25 | 25 | final read = article.isRead; |
| 26 | 26 | |
| @@ -20,7 +20,7 @@ class ArticleTile extends StatelessWidget { | |||
| 20 | 20 | ||
| 21 | @override | 21 | @override |
| 22 | Widget build(BuildContext context) { | 22 | Widget build(BuildContext context) { |
| 23 | - final c = GleanColors.of(context); | 23 | + final c = PulseboardColors.of(context); |
| 24 | final text = Theme.of(context).textTheme; | 24 | final text = Theme.of(context).textTheme; |
| 25 | final read = article.isRead; | 25 | final read = article.isRead; |
| 26 | 26 | ||
modified
app/lib/src/widgets/common.dart +13 -13 | @@ -5,8 +5,8 @@ import '../theme.dart'; | ||
| 5 | 5 | |
| 6 | 6 | /// A hard-edged box with a 2px border and the offset shadow the web app uses |
| 7 | 7 | /// for cards and buttons. |
| 8 | -class GleanBox extends StatelessWidget { | |
| 9 | - const GleanBox({ | |
| 8 | +class PulseboardBox extends StatelessWidget { | |
| 9 | + const PulseboardBox({ | |
| 10 | 10 | super.key, |
| 11 | 11 | required this.child, |
| 12 | 12 | this.padding = const EdgeInsets.all(12), |
| @@ -23,7 +23,7 @@ class GleanBox extends StatelessWidget { | ||
| 23 | 23 | |
| 24 | 24 | @override |
| 25 | 25 | Widget build(BuildContext context) { |
| 26 | - final c = GleanColors.of(context); | |
| 26 | + final c = PulseboardColors.of(context); | |
| 27 | 27 | final box = Container( |
| 28 | 28 | padding: padding, |
| 29 | 29 | decoration: BoxDecoration( |
| @@ -41,15 +41,15 @@ class GleanBox extends StatelessWidget { | ||
| 41 | 41 | } |
| 42 | 42 | |
| 43 | 43 | /// Monospace pill used for counts, categories and other metadata. |
| 44 | -class GleanTag extends StatelessWidget { | |
| 45 | - const GleanTag(this.label, {super.key, this.emphasis = false}); | |
| 44 | +class PulseboardTag extends StatelessWidget { | |
| 45 | + const PulseboardTag(this.label, {super.key, this.emphasis = false}); | |
| 46 | 46 | |
| 47 | 47 | final String label; |
| 48 | 48 | final bool emphasis; |
| 49 | 49 | |
| 50 | 50 | @override |
| 51 | 51 | Widget build(BuildContext context) { |
| 52 | - final c = GleanColors.of(context); | |
| 52 | + final c = PulseboardColors.of(context); | |
| 53 | 53 | return Container( |
| 54 | 54 | padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), |
| 55 | 55 | decoration: BoxDecoration( |
| @@ -68,8 +68,8 @@ class GleanTag extends StatelessWidget { | ||
| 68 | 68 | } |
| 69 | 69 | |
| 70 | 70 | /// Square-cornered primary button. |
| 71 | -class GleanButton extends StatelessWidget { | |
| 72 | - const GleanButton({ | |
| 71 | +class PulseboardButton extends StatelessWidget { | |
| 72 | + const PulseboardButton({ | |
| 73 | 73 | super.key, |
| 74 | 74 | required this.label, |
| 75 | 75 | this.onPressed, |
| @@ -84,11 +84,11 @@ class GleanButton extends StatelessWidget { | ||
| 84 | 84 | |
| 85 | 85 | @override |
| 86 | 86 | Widget build(BuildContext context) { |
| 87 | - final c = GleanColors.of(context); | |
| 87 | + final c = PulseboardColors.of(context); | |
| 88 | 88 | final enabled = onPressed != null && !busy; |
| 89 | 89 | return Opacity( |
| 90 | 90 | opacity: enabled ? 1 : 0.5, |
| 91 | - child: GleanBox( | |
| 91 | + child: PulseboardBox( | |
| 92 | 92 | onTap: enabled ? onPressed : null, |
| 93 | 93 | filled: !accent, |
| 94 | 94 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), |
| @@ -171,7 +171,7 @@ class ErrorView extends StatelessWidget { | ||
| 171 | 171 | |
| 172 | 172 | @override |
| 173 | 173 | Widget build(BuildContext context) { |
| 174 | - final c = GleanColors.of(context); | |
| 174 | + final c = PulseboardColors.of(context); | |
| 175 | 175 | return Center( |
| 176 | 176 | child: Padding( |
| 177 | 177 | padding: const EdgeInsets.all(24), |
| @@ -187,7 +187,7 @@ class ErrorView extends StatelessWidget { | ||
| 187 | 187 | const SizedBox(height: 8), |
| 188 | 188 | Text(message, textAlign: TextAlign.center), |
| 189 | 189 | const SizedBox(height: 16), |
| 190 | - GleanButton(label: 'Retry', onPressed: onRetry), | |
| 190 | + PulseboardButton(label: 'Retry', onPressed: onRetry), | |
| 191 | 191 | ], |
| 192 | 192 | ), |
| 193 | 193 | ), |
| @@ -226,7 +226,7 @@ class FaviconBadge extends StatelessWidget { | ||
| 226 | 226 | |
| 227 | 227 | @override |
| 228 | 228 | Widget build(BuildContext context) { |
| 229 | - final c = GleanColors.of(context); | |
| 229 | + final c = PulseboardColors.of(context); | |
| 230 | 230 | final letter = seed.isEmpty ? '?' : seed.characters.first.toUpperCase(); |
| 231 | 231 | final fallback = Container( |
| 232 | 232 | width: size, |
| @@ -5,8 +5,8 @@ import '../theme.dart'; | |||
| 5 | 5 | ||
| 6 | /// A hard-edged box with a 2px border and the offset shadow the web app uses | 6 | /// A hard-edged box with a 2px border and the offset shadow the web app uses |
| 7 | /// for cards and buttons. | 7 | /// for cards and buttons. |
| 8 | -class GleanBox extends StatelessWidget { | 8 | +class PulseboardBox extends StatelessWidget { |
| 9 | - const GleanBox({ | 9 | + const PulseboardBox({ |
| 10 | super.key, | 10 | super.key, |
| 11 | required this.child, | 11 | required this.child, |
| 12 | this.padding = const EdgeInsets.all(12), | 12 | this.padding = const EdgeInsets.all(12), |
| @@ -23,7 +23,7 @@ class GleanBox extends StatelessWidget { | |||
| 23 | 23 | ||
| 24 | @override | 24 | @override |
| 25 | Widget build(BuildContext context) { | 25 | Widget build(BuildContext context) { |
| 26 | - final c = GleanColors.of(context); | 26 | + final c = PulseboardColors.of(context); |
| 27 | final box = Container( | 27 | final box = Container( |
| 28 | padding: padding, | 28 | padding: padding, |
| 29 | decoration: BoxDecoration( | 29 | decoration: BoxDecoration( |
| @@ -41,15 +41,15 @@ class GleanBox extends StatelessWidget { | |||
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | /// Monospace pill used for counts, categories and other metadata. | 43 | /// Monospace pill used for counts, categories and other metadata. |
| 44 | -class GleanTag extends StatelessWidget { | 44 | +class PulseboardTag extends StatelessWidget { |
| 45 | - const GleanTag(this.label, {super.key, this.emphasis = false}); | 45 | + const PulseboardTag(this.label, {super.key, this.emphasis = false}); |
| 46 | 46 | ||
| 47 | final String label; | 47 | final String label; |
| 48 | final bool emphasis; | 48 | final bool emphasis; |
| 49 | 49 | ||
| 50 | @override | 50 | @override |
| 51 | Widget build(BuildContext context) { | 51 | Widget build(BuildContext context) { |
| 52 | - final c = GleanColors.of(context); | 52 | + final c = PulseboardColors.of(context); |
| 53 | return Container( | 53 | return Container( |
| 54 | padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), | 54 | padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), |
| 55 | decoration: BoxDecoration( | 55 | decoration: BoxDecoration( |
| @@ -68,8 +68,8 @@ class GleanTag extends StatelessWidget { | |||
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | /// Square-cornered primary button. | 70 | /// Square-cornered primary button. |
| 71 | -class GleanButton extends StatelessWidget { | 71 | +class PulseboardButton extends StatelessWidget { |
| 72 | - const GleanButton({ | 72 | + const PulseboardButton({ |
| 73 | super.key, | 73 | super.key, |
| 74 | required this.label, | 74 | required this.label, |
| 75 | this.onPressed, | 75 | this.onPressed, |
| @@ -84,11 +84,11 @@ class GleanButton extends StatelessWidget { | |||
| 84 | 84 | ||
| 85 | @override | 85 | @override |
| 86 | Widget build(BuildContext context) { | 86 | Widget build(BuildContext context) { |
| 87 | - final c = GleanColors.of(context); | 87 | + final c = PulseboardColors.of(context); |
| 88 | final enabled = onPressed != null && !busy; | 88 | final enabled = onPressed != null && !busy; |
| 89 | return Opacity( | 89 | return Opacity( |
| 90 | opacity: enabled ? 1 : 0.5, | 90 | opacity: enabled ? 1 : 0.5, |
| 91 | - child: GleanBox( | 91 | + child: PulseboardBox( |
| 92 | onTap: enabled ? onPressed : null, | 92 | onTap: enabled ? onPressed : null, |
| 93 | filled: !accent, | 93 | filled: !accent, |
| 94 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), | 94 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), |
| @@ -171,7 +171,7 @@ class ErrorView extends StatelessWidget { | |||
| 171 | 171 | ||
| 172 | @override | 172 | @override |
| 173 | Widget build(BuildContext context) { | 173 | Widget build(BuildContext context) { |
| 174 | - final c = GleanColors.of(context); | 174 | + final c = PulseboardColors.of(context); |
| 175 | return Center( | 175 | return Center( |
| 176 | child: Padding( | 176 | child: Padding( |
| 177 | padding: const EdgeInsets.all(24), | 177 | padding: const EdgeInsets.all(24), |
| @@ -187,7 +187,7 @@ class ErrorView extends StatelessWidget { | |||
| 187 | const SizedBox(height: 8), | 187 | const SizedBox(height: 8), |
| 188 | Text(message, textAlign: TextAlign.center), | 188 | Text(message, textAlign: TextAlign.center), |
| 189 | const SizedBox(height: 16), | 189 | const SizedBox(height: 16), |
| 190 | - GleanButton(label: 'Retry', onPressed: onRetry), | 190 | + PulseboardButton(label: 'Retry', onPressed: onRetry), |
| 191 | ], | 191 | ], |
| 192 | ), | 192 | ), |
| 193 | ), | 193 | ), |
| @@ -226,7 +226,7 @@ class FaviconBadge extends StatelessWidget { | |||
| 226 | 226 | ||
| 227 | @override | 227 | @override |
| 228 | Widget build(BuildContext context) { | 228 | Widget build(BuildContext context) { |
| 229 | - final c = GleanColors.of(context); | 229 | + final c = PulseboardColors.of(context); |
| 230 | final letter = seed.isEmpty ? '?' : seed.characters.first.toUpperCase(); | 230 | final letter = seed.isEmpty ? '?' : seed.characters.first.toUpperCase(); |
| 231 | final fallback = Container( | 231 | final fallback = Container( |
| 232 | width: size, | 232 | width: size, |
modified
app/linux/CMakeLists.txt +2 -2 | @@ -4,10 +4,10 @@ project(runner LANGUAGES CXX) | ||
| 4 | 4 | |
| 5 | 5 | # The name of the executable created for the application. Change this to change |
| 6 | 6 | # the on-disk name of your application. |
| 7 | -set(BINARY_NAME "glean_app") | |
| 7 | +set(BINARY_NAME "pulseboard_app") | |
| 8 | 8 | # The unique GTK application identifier for this application. See: |
| 9 | 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID |
| 10 | -set(APPLICATION_ID "fr.rbrt.glean.glean_app") | |
| 10 | +set(APPLICATION_ID "fr.rbrt.pulseboard.pulseboard_app") | |
| 11 | 11 | |
| 12 | 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent |
| 13 | 13 | # versions of CMake. |
| @@ -4,10 +4,10 @@ project(runner LANGUAGES CXX) | |||
| 4 | 4 | ||
| 5 | # The name of the executable created for the application. Change this to change | 5 | # The name of the executable created for the application. Change this to change |
| 6 | # the on-disk name of your application. | 6 | # the on-disk name of your application. |
| 7 | -set(BINARY_NAME "glean_app") | 7 | +set(BINARY_NAME "pulseboard_app") |
| 8 | # The unique GTK application identifier for this application. See: | 8 | # The unique GTK application identifier for this application. See: |
| 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID | 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID |
| 10 | -set(APPLICATION_ID "fr.rbrt.glean.glean_app") | 10 | +set(APPLICATION_ID "fr.rbrt.pulseboard.pulseboard_app") |
| 11 | 11 | ||
| 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent | 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent |
| 13 | # versions of CMake. | 13 | # versions of CMake. |
modified
app/linux/runner/my_application.cc +2 -2 | @@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) { | ||
| 45 | 45 | if (use_header_bar) { |
| 46 | 46 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); |
| 47 | 47 | gtk_widget_show(GTK_WIDGET(header_bar)); |
| 48 | - gtk_header_bar_set_title(header_bar, "glean_app"); | |
| 48 | + gtk_header_bar_set_title(header_bar, "pulseboard_app"); | |
| 49 | 49 | gtk_header_bar_set_show_close_button(header_bar, TRUE); |
| 50 | 50 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); |
| 51 | 51 | } else { |
| 52 | - gtk_window_set_title(window, "glean_app"); | |
| 52 | + gtk_window_set_title(window, "pulseboard_app"); | |
| 53 | 53 | } |
| 54 | 54 | |
| 55 | 55 | gtk_window_set_default_size(window, 1280, 720); |
| @@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) { | |||
| 45 | if (use_header_bar) { | 45 | if (use_header_bar) { |
| 46 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); | 46 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); |
| 47 | gtk_widget_show(GTK_WIDGET(header_bar)); | 47 | gtk_widget_show(GTK_WIDGET(header_bar)); |
| 48 | - gtk_header_bar_set_title(header_bar, "glean_app"); | 48 | + gtk_header_bar_set_title(header_bar, "pulseboard_app"); |
| 49 | gtk_header_bar_set_show_close_button(header_bar, TRUE); | 49 | gtk_header_bar_set_show_close_button(header_bar, TRUE); |
| 50 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); | 50 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); |
| 51 | } else { | 51 | } else { |
| 52 | - gtk_window_set_title(window, "glean_app"); | 52 | + gtk_window_set_title(window, "pulseboard_app"); |
| 53 | } | 53 | } |
| 54 | 54 | ||
| 55 | gtk_window_set_default_size(window, 1280, 720); | 55 | gtk_window_set_default_size(window, 1280, 720); |
modified
app/pubspec.yaml +1 -1 | @@ -1,4 +1,4 @@ | ||
| 1 | -name: glean_app | |
| 1 | +name: pulseboard_app | |
| 2 | 2 | description: "A new Flutter project." |
| 3 | 3 | # The following line prevents the package from being accidentally published to |
| 4 | 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. |
| @@ -1,4 +1,4 @@ | |||
| 1 | -name: glean_app | 1 | +name: pulseboard_app |
| 2 | description: "A new Flutter project." | 2 | description: "A new Flutter project." |
| 3 | # The following line prevents the package from being accidentally published to | 3 | # The following line prevents the package from being accidentally published to |
| 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. | 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. |
modified
app/test/live_reader_test.dart +5 -5 | @@ -4,18 +4,18 @@ library; | ||
| 4 | 4 | import 'dart:io'; |
| 5 | 5 | |
| 6 | 6 | import 'package:flutter_test/flutter_test.dart'; |
| 7 | -import 'package:glean_app/src/api/client.dart'; | |
| 7 | +import 'package:pulseboard_app/src/api/client.dart'; | |
| 8 | 8 | |
| 9 | 9 | /// Checks the client against a running reader, which is the only thing that |
| 10 | 10 | /// proves the two agree on field names. A mock returns whatever the test |
| 11 | 11 | /// author believed the server sends. |
| 12 | 12 | /// |
| 13 | -/// flutter test --tags live --dart-define=GLEAN_BASE_URL=http://127.0.0.1:8080 | |
| 13 | +/// flutter test --tags live --dart-define=PULSEBOARD_BASE_URL=http://127.0.0.1:8080 | |
| 14 | 14 | void main() { |
| 15 | - const base = String.fromEnvironment('GLEAN_BASE_URL', | |
| 15 | + const base = String.fromEnvironment('PULSEBOARD_BASE_URL', | |
| 16 | 16 | defaultValue: 'http://127.0.0.1:8080'); |
| 17 | 17 | |
| 18 | - late GleanClient client; | |
| 18 | + late PulseboardClient client; | |
| 19 | 19 | |
| 20 | 20 | setUpAll(() { |
| 21 | 21 | TestWidgetsFlutterBinding.ensureInitialized(); |
| @@ -24,7 +24,7 @@ void main() { | ||
| 24 | 24 | HttpOverrides.global = null; |
| 25 | 25 | }); |
| 26 | 26 | |
| 27 | - setUp(() => client = GleanClient(baseUrl: base)); | |
| 27 | + setUp(() => client = PulseboardClient(baseUrl: base)); | |
| 28 | 28 | tearDown(() => client.close()); |
| 29 | 29 | |
| 30 | 30 | test('feeds and unread parse', () async { |
| @@ -4,18 +4,18 @@ library; | |||
| 4 | import 'dart:io'; | 4 | import 'dart:io'; |
| 5 | 5 | ||
| 6 | import 'package:flutter_test/flutter_test.dart'; | 6 | import 'package:flutter_test/flutter_test.dart'; |
| 7 | -import 'package:glean_app/src/api/client.dart'; | 7 | +import 'package:pulseboard_app/src/api/client.dart'; |
| 8 | 8 | ||
| 9 | /// Checks the client against a running reader, which is the only thing that | 9 | /// Checks the client against a running reader, which is the only thing that |
| 10 | /// proves the two agree on field names. A mock returns whatever the test | 10 | /// proves the two agree on field names. A mock returns whatever the test |
| 11 | /// author believed the server sends. | 11 | /// author believed the server sends. |
| 12 | /// | 12 | /// |
| 13 | -/// flutter test --tags live --dart-define=GLEAN_BASE_URL=http://127.0.0.1:8080 | 13 | +/// flutter test --tags live --dart-define=PULSEBOARD_BASE_URL=http://127.0.0.1:8080 |
| 14 | void main() { | 14 | void main() { |
| 15 | - const base = String.fromEnvironment('GLEAN_BASE_URL', | 15 | + const base = String.fromEnvironment('PULSEBOARD_BASE_URL', |
| 16 | defaultValue: 'http://127.0.0.1:8080'); | 16 | defaultValue: 'http://127.0.0.1:8080'); |
| 17 | 17 | ||
| 18 | - late GleanClient client; | 18 | + late PulseboardClient client; |
| 19 | 19 | ||
| 20 | setUpAll(() { | 20 | setUpAll(() { |
| 21 | TestWidgetsFlutterBinding.ensureInitialized(); | 21 | TestWidgetsFlutterBinding.ensureInitialized(); |
| @@ -24,7 +24,7 @@ void main() { | |||
| 24 | HttpOverrides.global = null; | 24 | HttpOverrides.global = null; |
| 25 | }); | 25 | }); |
| 26 | 26 | ||
| 27 | - setUp(() => client = GleanClient(baseUrl: base)); | 27 | + setUp(() => client = PulseboardClient(baseUrl: base)); |
| 28 | tearDown(() => client.close()); | 28 | tearDown(() => client.close()); |
| 29 | 29 | ||
| 30 | test('feeds and unread parse', () async { | 30 | test('feeds and unread parse', () async { |
modified
app/test/widget_test.dart +6 -6 | @@ -5,10 +5,10 @@ import 'package:flutter_test/flutter_test.dart'; | ||
| 5 | 5 | import 'package:http/http.dart' as http; |
| 6 | 6 | import 'package:http/testing.dart'; |
| 7 | 7 | |
| 8 | -import 'package:glean_app/src/api/client.dart'; | |
| 9 | -import 'package:glean_app/src/app_state.dart'; | |
| 10 | -import 'package:glean_app/src/screens/home_shell.dart'; | |
| 11 | -import 'package:glean_app/src/theme.dart'; | |
| 8 | +import 'package:pulseboard_app/src/api/client.dart'; | |
| 9 | +import 'package:pulseboard_app/src/app_state.dart'; | |
| 10 | +import 'package:pulseboard_app/src/screens/home_shell.dart'; | |
| 11 | +import 'package:pulseboard_app/src/theme.dart'; | |
| 12 | 12 | |
| 13 | 13 | /// A stand-in for the reader, covering the routes the shell touches. |
| 14 | 14 | http.Client fakeReader({List<String>? seen, bool empty = false}) { |
| @@ -66,14 +66,14 @@ http.Client fakeReader({List<String>? seen, bool empty = false}) { | ||
| 66 | 66 | Future<AppState> pump(WidgetTester tester, |
| 67 | 67 | {List<String>? seen, bool empty = false}) async { |
| 68 | 68 | final state = AppState( |
| 69 | - client: GleanClient( | |
| 69 | + client: PulseboardClient( | |
| 70 | 70 | baseUrl: 'https://reader.test', |
| 71 | 71 | client: fakeReader(seen: seen, empty: empty)), |
| 72 | 72 | ); |
| 73 | 73 | await tester.pumpWidget(AppScope( |
| 74 | 74 | state: state, |
| 75 | 75 | child: MaterialApp( |
| 76 | - theme: gleanTheme(Brightness.light), | |
| 76 | + theme: pulseboardTheme(Brightness.light), | |
| 77 | 77 | home: const HomeShell(), |
| 78 | 78 | ), |
| 79 | 79 | )); |
| @@ -5,10 +5,10 @@ import 'package:flutter_test/flutter_test.dart'; | |||
| 5 | import 'package:http/http.dart' as http; | 5 | import 'package:http/http.dart' as http; |
| 6 | import 'package:http/testing.dart'; | 6 | import 'package:http/testing.dart'; |
| 7 | 7 | ||
| 8 | -import 'package:glean_app/src/api/client.dart'; | 8 | +import 'package:pulseboard_app/src/api/client.dart'; |
| 9 | -import 'package:glean_app/src/app_state.dart'; | 9 | +import 'package:pulseboard_app/src/app_state.dart'; |
| 10 | -import 'package:glean_app/src/screens/home_shell.dart'; | 10 | +import 'package:pulseboard_app/src/screens/home_shell.dart'; |
| 11 | -import 'package:glean_app/src/theme.dart'; | 11 | +import 'package:pulseboard_app/src/theme.dart'; |
| 12 | 12 | ||
| 13 | /// A stand-in for the reader, covering the routes the shell touches. | 13 | /// A stand-in for the reader, covering the routes the shell touches. |
| 14 | http.Client fakeReader({List<String>? seen, bool empty = false}) { | 14 | http.Client fakeReader({List<String>? seen, bool empty = false}) { |
| @@ -66,14 +66,14 @@ http.Client fakeReader({List<String>? seen, bool empty = false}) { | |||
| 66 | Future<AppState> pump(WidgetTester tester, | 66 | Future<AppState> pump(WidgetTester tester, |
| 67 | {List<String>? seen, bool empty = false}) async { | 67 | {List<String>? seen, bool empty = false}) async { |
| 68 | final state = AppState( | 68 | final state = AppState( |
| 69 | - client: GleanClient( | 69 | + client: PulseboardClient( |
| 70 | baseUrl: 'https://reader.test', | 70 | baseUrl: 'https://reader.test', |
| 71 | client: fakeReader(seen: seen, empty: empty)), | 71 | client: fakeReader(seen: seen, empty: empty)), |
| 72 | ); | 72 | ); |
| 73 | await tester.pumpWidget(AppScope( | 73 | await tester.pumpWidget(AppScope( |
| 74 | state: state, | 74 | state: state, |
| 75 | child: MaterialApp( | 75 | child: MaterialApp( |
| 76 | - theme: gleanTheme(Brightness.light), | 76 | + theme: pulseboardTheme(Brightness.light), |
| 77 | home: const HomeShell(), | 77 | home: const HomeShell(), |
| 78 | ), | 78 | ), |
| 79 | )); | 79 | )); |
modified
deploy/modal_app.py +25 -25 | @@ -29,14 +29,14 @@ import modal | ||
| 29 | 29 | |
| 30 | 30 | REPO = pathlib.Path(__file__).parent.parent |
| 31 | 31 | |
| 32 | -APP_NAME = "glean-reader" | |
| 32 | +APP_NAME = "pulseboard" | |
| 33 | 33 | PORT = 8080 |
| 34 | -PUBLIC_URL = "https://codegod100--glean-reader-serve.modal.run" | |
| 34 | +PUBLIC_URL = "https://codegod100--pulseboard-serve.modal.run" | |
| 35 | 35 | |
| 36 | 36 | VOLUME_PATH = "/data" |
| 37 | 37 | SNAPSHOT_DIR = f"{VOLUME_PATH}/db" |
| 38 | 38 | LIVE_DIR = "/livedb" |
| 39 | -DB_BASE = f"{LIVE_DIR}/glean" | |
| 39 | +DB_BASE = f"{LIVE_DIR}/pulseboard" | |
| 40 | 40 | |
| 41 | 41 | # The reader opens <base>_users and attaches <base>_articles. |
| 42 | 42 | DB_SUFFIXES = ("_users", "_articles") |
| @@ -57,28 +57,28 @@ image = ( | ||
| 57 | 57 | REPO / "spike" / "nim", |
| 58 | 58 | remote_path="/src", |
| 59 | 59 | copy=True, |
| 60 | - ignore=["nimcache", "glean", "*_probe", "*.db*"], | |
| 60 | + ignore=["nimcache", "pulseboard", "*_probe", "*.db*"], | |
| 61 | 61 | ) |
| 62 | 62 | # The Flutter web build is committed output rather than built here: |
| 63 | 63 | # installing Flutter in the image would add gigabytes and minutes for |
| 64 | 64 | # something the developer machine has already produced. |
| 65 | 65 | .add_local_dir(REPO / "app" / "build" / "web", remote_path="/web", copy=True) |
| 66 | - .run_commands("cd /src && nim c -d:release -d:ssl --hints:off -o:/usr/local/bin/glean glean.nim") | |
| 66 | + .run_commands("cd /src && nim c -d:release -d:ssl --hints:off -o:/usr/local/bin/pulseboard pulseboard.nim") | |
| 67 | 67 | ) |
| 68 | 68 | |
| 69 | -volume = modal.Volume.from_name("glean-reader-data", create_if_missing=True) | |
| 69 | +volume = modal.Volume.from_name("pulseboard-data", create_if_missing=True) | |
| 70 | 70 | |
| 71 | -# GLEAN_TOKEN. Without it the reader binds loopback and Modal's proxy reaches | |
| 71 | +# PULSEBOARD_TOKEN. Without it the reader binds loopback and Modal's proxy reaches | |
| 72 | 72 | # nothing, so this is required rather than optional: |
| 73 | -# modal secret create glean-reader GLEAN_TOKEN="$(openssl rand -hex 24)" | |
| 74 | -secret = modal.Secret.from_name("glean-reader") | |
| 73 | +# modal secret create pulseboard PULSEBOARD_TOKEN="$(openssl rand -hex 24)" | |
| 74 | +secret = modal.Secret.from_name("pulseboard") | |
| 75 | 75 | |
| 76 | 76 | app = modal.App(APP_NAME) |
| 77 | 77 | |
| 78 | 78 | |
| 79 | 79 | @app.function(image=image, secrets=[secret], timeout=300) |
| 80 | 80 | def import_opml(opml: str) -> str: |
| 81 | - """Import a backup through the live API without revealing GLEAN_TOKEN. | |
| 81 | + """Import a backup through the live API without revealing PULSEBOARD_TOKEN. | |
| 82 | 82 | |
| 83 | 83 | Run with ``modal run deploy/modal_app.py::import_opml --opml "$(<backup.opml)"``. |
| 84 | 84 | Keeping this as a Modal function means only workspace members can invoke |
| @@ -87,11 +87,11 @@ def import_opml(opml: str) -> str: | ||
| 87 | 87 | from urllib.parse import urlencode |
| 88 | 88 | from urllib.request import Request, urlopen |
| 89 | 89 | |
| 90 | - token = os.environ["GLEAN_TOKEN"] | |
| 90 | + token = os.environ["PULSEBOARD_TOKEN"] | |
| 91 | 91 | request = Request( |
| 92 | 92 | f"{PUBLIC_URL}/opml", |
| 93 | 93 | data=urlencode({"opml": opml}).encode(), |
| 94 | - headers={"X-Glean-Token": token}, | |
| 94 | + headers={"X-Pulseboard-Token": token}, | |
| 95 | 95 | method="POST", |
| 96 | 96 | ) |
| 97 | 97 | with urlopen(request, timeout=240) as response: |
| @@ -103,7 +103,7 @@ def inspect_feeds() -> str: | ||
| 103 | 103 | """Return live feed and unread-article data for a recovery check.""" |
| 104 | 104 | from urllib.request import Request, urlopen |
| 105 | 105 | |
| 106 | - headers = {"X-Glean-Token": os.environ["GLEAN_TOKEN"]} | |
| 106 | + headers = {"X-Pulseboard-Token": os.environ["PULSEBOARD_TOKEN"]} | |
| 107 | 107 | results = {} |
| 108 | 108 | for path in ("/feeds", "/articles?status=unread&limit=5"): |
| 109 | 109 | with urlopen(Request(f"{PUBLIC_URL}{path}", headers=headers), timeout=50) as response: |
| @@ -132,7 +132,7 @@ def _snapshot_once() -> None: | ||
| 132 | 132 | subprocess.run( |
| 133 | 133 | ["sqlite3", live, f"VACUUM INTO '{staged}'"], check=True, capture_output=True |
| 134 | 134 | ) |
| 135 | - shutil.copy2(staged, f"{SNAPSHOT_DIR}/glean{suffix}") | |
| 135 | + shutil.copy2(staged, f"{SNAPSHOT_DIR}/pulseboard{suffix}") | |
| 136 | 136 | os.remove(staged) |
| 137 | 137 | volume.commit() |
| 138 | 138 | |
| @@ -140,10 +140,10 @@ def _snapshot_once() -> None: | ||
| 140 | 140 | def _restore() -> None: |
| 141 | 141 | os.makedirs(LIVE_DIR, exist_ok=True) |
| 142 | 142 | for suffix in DB_SUFFIXES: |
| 143 | - snap = f"{SNAPSHOT_DIR}/glean{suffix}" | |
| 143 | + snap = f"{SNAPSHOT_DIR}/pulseboard{suffix}" | |
| 144 | 144 | if os.path.exists(snap): |
| 145 | 145 | shutil.copy2(snap, f"{DB_BASE}{suffix}") |
| 146 | - print(f"[glean] restored {snap}", flush=True) | |
| 146 | + print(f"[pulseboard] restored {snap}", flush=True) | |
| 147 | 147 | |
| 148 | 148 | |
| 149 | 149 | def _snapshot_loop(stop: threading.Event) -> None: |
| @@ -151,7 +151,7 @@ def _snapshot_loop(stop: threading.Event) -> None: | ||
| 151 | 151 | try: |
| 152 | 152 | _snapshot_once() |
| 153 | 153 | except Exception as exc: # a failed snapshot must not kill the server |
| 154 | - print(f"[glean] snapshot failed: {exc}", file=sys.stderr, flush=True) | |
| 154 | + print(f"[pulseboard] snapshot failed: {exc}", file=sys.stderr, flush=True) | |
| 155 | 155 | |
| 156 | 156 | |
| 157 | 157 | @app.function( |
| @@ -170,16 +170,16 @@ def serve() -> None: | ||
| 170 | 170 | _restore() |
| 171 | 171 | |
| 172 | 172 | env = dict(os.environ) |
| 173 | - env["GLEAN_DB"] = DB_BASE | |
| 174 | - env["GLEAN_PORT"] = str(PORT) | |
| 175 | - env["GLEAN_WEB"] = "/web" | |
| 176 | - if not env.get("GLEAN_TOKEN"): | |
| 173 | + env["PULSEBOARD_DB"] = DB_BASE | |
| 174 | + env["PULSEBOARD_PORT"] = str(PORT) | |
| 175 | + env["PULSEBOARD_WEB"] = "/web" | |
| 176 | + if not env.get("PULSEBOARD_TOKEN"): | |
| 177 | 177 | # The reader would bind loopback and Modal's proxy would reach |
| 178 | 178 | # nothing, which looks like a broken deploy rather than a missing |
| 179 | 179 | # secret. Fail with the reason instead. |
| 180 | - raise RuntimeError("GLEAN_TOKEN is required: the reader has no other auth") | |
| 180 | + raise RuntimeError("PULSEBOARD_TOKEN is required: the reader has no other auth") | |
| 181 | 181 | |
| 182 | - proc = subprocess.Popen(["/usr/local/bin/glean"], env=env) | |
| 182 | + proc = subprocess.Popen(["/usr/local/bin/pulseboard"], env=env) | |
| 183 | 183 | |
| 184 | 184 | stop = threading.Event() |
| 185 | 185 | threading.Thread(target=_snapshot_loop, args=(stop,), daemon=True).start() |
| @@ -190,9 +190,9 @@ def serve() -> None: | ||
| 190 | 190 | try: |
| 191 | 191 | time.sleep(1) |
| 192 | 192 | _snapshot_once() |
| 193 | - print("[glean] final snapshot written", flush=True) | |
| 193 | + print("[pulseboard] final snapshot written", flush=True) | |
| 194 | 194 | except Exception as exc: |
| 195 | - print(f"[glean] final snapshot failed: {exc}", file=sys.stderr, flush=True) | |
| 195 | + print(f"[pulseboard] final snapshot failed: {exc}", file=sys.stderr, flush=True) | |
| 196 | 196 | sys.exit(0) |
| 197 | 197 | |
| 198 | 198 | signal.signal(signal.SIGTERM, shutdown) |
| @@ -29,14 +29,14 @@ import modal | |||
| 29 | 29 | ||
| 30 | REPO = pathlib.Path(__file__).parent.parent | 30 | REPO = pathlib.Path(__file__).parent.parent |
| 31 | 31 | ||
| 32 | -APP_NAME = "glean-reader" | 32 | +APP_NAME = "pulseboard" |
| 33 | PORT = 8080 | 33 | PORT = 8080 |
| 34 | -PUBLIC_URL = "https://codegod100--glean-reader-serve.modal.run" | 34 | +PUBLIC_URL = "https://codegod100--pulseboard-serve.modal.run" |
| 35 | 35 | ||
| 36 | VOLUME_PATH = "/data" | 36 | VOLUME_PATH = "/data" |
| 37 | SNAPSHOT_DIR = f"{VOLUME_PATH}/db" | 37 | SNAPSHOT_DIR = f"{VOLUME_PATH}/db" |
| 38 | LIVE_DIR = "/livedb" | 38 | LIVE_DIR = "/livedb" |
| 39 | -DB_BASE = f"{LIVE_DIR}/glean" | 39 | +DB_BASE = f"{LIVE_DIR}/pulseboard" |
| 40 | 40 | ||
| 41 | # The reader opens <base>_users and attaches <base>_articles. | 41 | # The reader opens <base>_users and attaches <base>_articles. |
| 42 | DB_SUFFIXES = ("_users", "_articles") | 42 | DB_SUFFIXES = ("_users", "_articles") |
| @@ -57,28 +57,28 @@ image = ( | |||
| 57 | REPO / "spike" / "nim", | 57 | REPO / "spike" / "nim", |
| 58 | remote_path="/src", | 58 | remote_path="/src", |
| 59 | copy=True, | 59 | copy=True, |
| 60 | - ignore=["nimcache", "glean", "*_probe", "*.db*"], | 60 | + ignore=["nimcache", "pulseboard", "*_probe", "*.db*"], |
| 61 | ) | 61 | ) |
| 62 | # The Flutter web build is committed output rather than built here: | 62 | # The Flutter web build is committed output rather than built here: |
| 63 | # installing Flutter in the image would add gigabytes and minutes for | 63 | # installing Flutter in the image would add gigabytes and minutes for |
| 64 | # something the developer machine has already produced. | 64 | # something the developer machine has already produced. |
| 65 | .add_local_dir(REPO / "app" / "build" / "web", remote_path="/web", copy=True) | 65 | .add_local_dir(REPO / "app" / "build" / "web", remote_path="/web", copy=True) |
| 66 | - .run_commands("cd /src && nim c -d:release -d:ssl --hints:off -o:/usr/local/bin/glean glean.nim") | 66 | + .run_commands("cd /src && nim c -d:release -d:ssl --hints:off -o:/usr/local/bin/pulseboard pulseboard.nim") |
| 67 | ) | 67 | ) |
| 68 | 68 | ||
| 69 | -volume = modal.Volume.from_name("glean-reader-data", create_if_missing=True) | 69 | +volume = modal.Volume.from_name("pulseboard-data", create_if_missing=True) |
| 70 | 70 | ||
| 71 | -# GLEAN_TOKEN. Without it the reader binds loopback and Modal's proxy reaches | 71 | +# PULSEBOARD_TOKEN. Without it the reader binds loopback and Modal's proxy reaches |
| 72 | # nothing, so this is required rather than optional: | 72 | # nothing, so this is required rather than optional: |
| 73 | -# modal secret create glean-reader GLEAN_TOKEN="$(openssl rand -hex 24)" | 73 | +# modal secret create pulseboard PULSEBOARD_TOKEN="$(openssl rand -hex 24)" |
| 74 | -secret = modal.Secret.from_name("glean-reader") | 74 | +secret = modal.Secret.from_name("pulseboard") |
| 75 | 75 | ||
| 76 | app = modal.App(APP_NAME) | 76 | app = modal.App(APP_NAME) |
| 77 | 77 | ||
| 78 | 78 | ||
| 79 | @app.function(image=image, secrets=[secret], timeout=300) | 79 | @app.function(image=image, secrets=[secret], timeout=300) |
| 80 | def import_opml(opml: str) -> str: | 80 | def import_opml(opml: str) -> str: |
| 81 | - """Import a backup through the live API without revealing GLEAN_TOKEN. | 81 | + """Import a backup through the live API without revealing PULSEBOARD_TOKEN. |
| 82 | 82 | ||
| 83 | Run with ``modal run deploy/modal_app.py::import_opml --opml "$(<backup.opml)"``. | 83 | Run with ``modal run deploy/modal_app.py::import_opml --opml "$(<backup.opml)"``. |
| 84 | Keeping this as a Modal function means only workspace members can invoke | 84 | Keeping this as a Modal function means only workspace members can invoke |
| @@ -87,11 +87,11 @@ def import_opml(opml: str) -> str: | |||
| 87 | from urllib.parse import urlencode | 87 | from urllib.parse import urlencode |
| 88 | from urllib.request import Request, urlopen | 88 | from urllib.request import Request, urlopen |
| 89 | 89 | ||
| 90 | - token = os.environ["GLEAN_TOKEN"] | 90 | + token = os.environ["PULSEBOARD_TOKEN"] |
| 91 | request = Request( | 91 | request = Request( |
| 92 | f"{PUBLIC_URL}/opml", | 92 | f"{PUBLIC_URL}/opml", |
| 93 | data=urlencode({"opml": opml}).encode(), | 93 | data=urlencode({"opml": opml}).encode(), |
| 94 | - headers={"X-Glean-Token": token}, | 94 | + headers={"X-Pulseboard-Token": token}, |
| 95 | method="POST", | 95 | method="POST", |
| 96 | ) | 96 | ) |
| 97 | with urlopen(request, timeout=240) as response: | 97 | with urlopen(request, timeout=240) as response: |
| @@ -103,7 +103,7 @@ def inspect_feeds() -> str: | |||
| 103 | """Return live feed and unread-article data for a recovery check.""" | 103 | """Return live feed and unread-article data for a recovery check.""" |
| 104 | from urllib.request import Request, urlopen | 104 | from urllib.request import Request, urlopen |
| 105 | 105 | ||
| 106 | - headers = {"X-Glean-Token": os.environ["GLEAN_TOKEN"]} | 106 | + headers = {"X-Pulseboard-Token": os.environ["PULSEBOARD_TOKEN"]} |
| 107 | results = {} | 107 | results = {} |
| 108 | for path in ("/feeds", "/articles?status=unread&limit=5"): | 108 | for path in ("/feeds", "/articles?status=unread&limit=5"): |
| 109 | with urlopen(Request(f"{PUBLIC_URL}{path}", headers=headers), timeout=50) as response: | 109 | with urlopen(Request(f"{PUBLIC_URL}{path}", headers=headers), timeout=50) as response: |
| @@ -132,7 +132,7 @@ def _snapshot_once() -> None: | |||
| 132 | subprocess.run( | 132 | subprocess.run( |
| 133 | ["sqlite3", live, f"VACUUM INTO '{staged}'"], check=True, capture_output=True | 133 | ["sqlite3", live, f"VACUUM INTO '{staged}'"], check=True, capture_output=True |
| 134 | ) | 134 | ) |
| 135 | - shutil.copy2(staged, f"{SNAPSHOT_DIR}/glean{suffix}") | 135 | + shutil.copy2(staged, f"{SNAPSHOT_DIR}/pulseboard{suffix}") |
| 136 | os.remove(staged) | 136 | os.remove(staged) |
| 137 | volume.commit() | 137 | volume.commit() |
| 138 | 138 | ||
| @@ -140,10 +140,10 @@ def _snapshot_once() -> None: | |||
| 140 | def _restore() -> None: | 140 | def _restore() -> None: |
| 141 | os.makedirs(LIVE_DIR, exist_ok=True) | 141 | os.makedirs(LIVE_DIR, exist_ok=True) |
| 142 | for suffix in DB_SUFFIXES: | 142 | for suffix in DB_SUFFIXES: |
| 143 | - snap = f"{SNAPSHOT_DIR}/glean{suffix}" | 143 | + snap = f"{SNAPSHOT_DIR}/pulseboard{suffix}" |
| 144 | if os.path.exists(snap): | 144 | if os.path.exists(snap): |
| 145 | shutil.copy2(snap, f"{DB_BASE}{suffix}") | 145 | shutil.copy2(snap, f"{DB_BASE}{suffix}") |
| 146 | - print(f"[glean] restored {snap}", flush=True) | 146 | + print(f"[pulseboard] restored {snap}", flush=True) |
| 147 | 147 | ||
| 148 | 148 | ||
| 149 | def _snapshot_loop(stop: threading.Event) -> None: | 149 | def _snapshot_loop(stop: threading.Event) -> None: |
| @@ -151,7 +151,7 @@ def _snapshot_loop(stop: threading.Event) -> None: | |||
| 151 | try: | 151 | try: |
| 152 | _snapshot_once() | 152 | _snapshot_once() |
| 153 | except Exception as exc: # a failed snapshot must not kill the server | 153 | except Exception as exc: # a failed snapshot must not kill the server |
| 154 | - print(f"[glean] snapshot failed: {exc}", file=sys.stderr, flush=True) | 154 | + print(f"[pulseboard] snapshot failed: {exc}", file=sys.stderr, flush=True) |
| 155 | 155 | ||
| 156 | 156 | ||
| 157 | @app.function( | 157 | @app.function( |
| @@ -170,16 +170,16 @@ def serve() -> None: | |||
| 170 | _restore() | 170 | _restore() |
| 171 | 171 | ||
| 172 | env = dict(os.environ) | 172 | env = dict(os.environ) |
| 173 | - env["GLEAN_DB"] = DB_BASE | 173 | + env["PULSEBOARD_DB"] = DB_BASE |
| 174 | - env["GLEAN_PORT"] = str(PORT) | 174 | + env["PULSEBOARD_PORT"] = str(PORT) |
| 175 | - env["GLEAN_WEB"] = "/web" | 175 | + env["PULSEBOARD_WEB"] = "/web" |
| 176 | - if not env.get("GLEAN_TOKEN"): | 176 | + if not env.get("PULSEBOARD_TOKEN"): |
| 177 | # The reader would bind loopback and Modal's proxy would reach | 177 | # The reader would bind loopback and Modal's proxy would reach |
| 178 | # nothing, which looks like a broken deploy rather than a missing | 178 | # nothing, which looks like a broken deploy rather than a missing |
| 179 | # secret. Fail with the reason instead. | 179 | # secret. Fail with the reason instead. |
| 180 | - raise RuntimeError("GLEAN_TOKEN is required: the reader has no other auth") | 180 | + raise RuntimeError("PULSEBOARD_TOKEN is required: the reader has no other auth") |
| 181 | 181 | ||
| 182 | - proc = subprocess.Popen(["/usr/local/bin/glean"], env=env) | 182 | + proc = subprocess.Popen(["/usr/local/bin/pulseboard"], env=env) |
| 183 | 183 | ||
| 184 | stop = threading.Event() | 184 | stop = threading.Event() |
| 185 | threading.Thread(target=_snapshot_loop, args=(stop,), daemon=True).start() | 185 | threading.Thread(target=_snapshot_loop, args=(stop,), daemon=True).start() |
| @@ -190,9 +190,9 @@ def serve() -> None: | |||
| 190 | try: | 190 | try: |
| 191 | time.sleep(1) | 191 | time.sleep(1) |
| 192 | _snapshot_once() | 192 | _snapshot_once() |
| 193 | - print("[glean] final snapshot written", flush=True) | 193 | + print("[pulseboard] final snapshot written", flush=True) |
| 194 | except Exception as exc: | 194 | except Exception as exc: |
| 195 | - print(f"[glean] final snapshot failed: {exc}", file=sys.stderr, flush=True) | 195 | + print(f"[pulseboard] final snapshot failed: {exc}", file=sys.stderr, flush=True) |
| 196 | sys.exit(0) | 196 | sys.exit(0) |
| 197 | 197 | ||
| 198 | signal.signal(signal.SIGTERM, shutdown) | 198 | signal.signal(signal.SIGTERM, shutdown) |
modified
deploy/readme.md +3 -3 | @@ -1,8 +1,8 @@ | ||
| 1 | 1 | # Deploying |
| 2 | 2 | |
| 3 | 3 | ``` |
| 4 | -modal secret create glean-reader GLEAN_TOKEN="$(openssl rand -hex 24)" | |
| 5 | -cd app && flutter build web --release --dart-define=GLEAN_BASE_URL= | |
| 4 | +modal secret create pulseboard PULSEBOARD_TOKEN="$(openssl rand -hex 24)" | |
| 5 | +cd app && flutter build web --release --dart-define=PULSEBOARD_BASE_URL= | |
| 6 | 6 | modal deploy deploy/modal_app.py |
| 7 | 7 | ``` |
| 8 | 8 | |
| @@ -23,7 +23,7 @@ the deployment ships the previous client. | ||
| 23 | 23 | |
| 24 | 24 | ## The token is not optional here |
| 25 | 25 | |
| 26 | -Without `GLEAN_TOKEN` the reader binds loopback, Modal's proxy reaches | |
| 26 | +Without `PULSEBOARD_TOKEN` the reader binds loopback, Modal's proxy reaches | |
| 27 | 27 | nothing, and the deploy looks broken rather than insecure. The function |
| 28 | 28 | raises with the reason instead of starting. |
| 29 | 29 | |
| @@ -1,8 +1,8 @@ | |||
| 1 | # Deploying | 1 | # Deploying |
| 2 | 2 | ||
| 3 | ``` | 3 | ``` |
| 4 | -modal secret create glean-reader GLEAN_TOKEN="$(openssl rand -hex 24)" | 4 | +modal secret create pulseboard PULSEBOARD_TOKEN="$(openssl rand -hex 24)" |
| 5 | -cd app && flutter build web --release --dart-define=GLEAN_BASE_URL= | 5 | +cd app && flutter build web --release --dart-define=PULSEBOARD_BASE_URL= |
| 6 | modal deploy deploy/modal_app.py | 6 | modal deploy deploy/modal_app.py |
| 7 | ``` | 7 | ``` |
| 8 | 8 | ||
| @@ -23,7 +23,7 @@ the deployment ships the previous client. | |||
| 23 | 23 | ||
| 24 | ## The token is not optional here | 24 | ## The token is not optional here |
| 25 | 25 | ||
| 26 | -Without `GLEAN_TOKEN` the reader binds loopback, Modal's proxy reaches | 26 | +Without `PULSEBOARD_TOKEN` the reader binds loopback, Modal's proxy reaches |
| 27 | nothing, and the deploy looks broken rather than insecure. The function | 27 | nothing, and the deploy looks broken rather than insecure. The function |
| 28 | raises with the reason instead of starting. | 28 | raises with the reason instead of starting. |
| 29 | 29 | ||
renamed
nandi-pulseboard-subscriptions.opml +1 -1 | similarity index 97% | ||
| rename from nandi-glean-subscriptions.opml | ||
| rename to nandi-pulseboard-subscriptions.opml | ||
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | <?xml version="1.0" encoding="UTF-8"?> |
| 2 | 2 | <opml version="2.0"> |
| 3 | - <head><title>Glean subscriptions — nandi.uk</title></head> | |
| 3 | + <head><title>Pulseboard subscriptions — nandi.uk</title></head> | |
| 4 | 4 | <body> |
| 5 | 5 | <outline text="(iterate think thoughts)" title="(iterate think thoughts)" type="rss" xmlUrl="https://yogthos.net/feed.xml"/> |
| 6 | 6 | <outline text="This Week in GNOME" title="This Week in GNOME" type="rss" xmlUrl="https://thisweek.gnome.org/index.xml"/> |
| similarity index 97% | |||
| rename from nandi-glean-subscriptions.opml | |||
| rename to nandi-pulseboard-subscriptions.opml | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | 1 | <?xml version="1.0" encoding="UTF-8"?> |
| 2 | <opml version="2.0"> | 2 | <opml version="2.0"> |
| 3 | - <head><title>Glean subscriptions — nandi.uk</title></head> | 3 | + <head><title>Pulseboard subscriptions — nandi.uk</title></head> |
| 4 | <body> | 4 | <body> |
| 5 | <outline text="(iterate think thoughts)" title="(iterate think thoughts)" type="rss" xmlUrl="https://yogthos.net/feed.xml"/> | 5 | <outline text="(iterate think thoughts)" title="(iterate think thoughts)" type="rss" xmlUrl="https://yogthos.net/feed.xml"/> |
| 6 | <outline text="This Week in GNOME" title="This Week in GNOME" type="rss" xmlUrl="https://thisweek.gnome.org/index.xml"/> | 6 | <outline text="This Week in GNOME" title="This Week in GNOME" type="rss" xmlUrl="https://thisweek.gnome.org/index.xml"/> |
modified
readme.md +4 -4 | @@ -1,4 +1,4 @@ | ||
| 1 | -# glean | |
| 1 | +# pulseboard | |
| 2 | 2 | |
| 3 | 3 | An RSS reader. Two pieces: |
| 4 | 4 | |
| @@ -10,15 +10,15 @@ An RSS reader. Two pieces: | ||
| 10 | 10 | Run the reader, point the client at it: |
| 11 | 11 | |
| 12 | 12 | ``` |
| 13 | -cd spike/nim && nim c -d:ssl glean.nim && ./glean | |
| 13 | +cd spike/nim && nim c -d:ssl pulseboard.nim && ./pulseboard | |
| 14 | 14 | ``` |
| 15 | 15 | |
| 16 | 16 | It listens on `127.0.0.1:8080`, keeps a SQLite database under |
| 17 | -`~/.local/share/glean/`, and serves a plain HTML interface at the root — so | |
| 17 | +`~/.local/share/pulseboard/`, and serves a plain HTML interface at the root — so | |
| 18 | 18 | it is usable on its own, without the Flutter client. |
| 19 | 19 | |
| 20 | 20 | ``` |
| 21 | -cd app && flutter run --dart-define=GLEAN_BASE_URL=http://127.0.0.1:8080 | |
| 21 | +cd app && flutter run --dart-define=PULSEBOARD_BASE_URL=http://127.0.0.1:8080 | |
| 22 | 22 | ``` |
| 23 | 23 | |
| 24 | 24 | ## Why it is split |
| @@ -1,4 +1,4 @@ | |||
| 1 | -# glean | 1 | +# pulseboard |
| 2 | 2 | ||
| 3 | An RSS reader. Two pieces: | 3 | An RSS reader. Two pieces: |
| 4 | 4 | ||
| @@ -10,15 +10,15 @@ An RSS reader. Two pieces: | |||
| 10 | Run the reader, point the client at it: | 10 | Run the reader, point the client at it: |
| 11 | 11 | ||
| 12 | ``` | 12 | ``` |
| 13 | -cd spike/nim && nim c -d:ssl glean.nim && ./glean | 13 | +cd spike/nim && nim c -d:ssl pulseboard.nim && ./pulseboard |
| 14 | ``` | 14 | ``` |
| 15 | 15 | ||
| 16 | It listens on `127.0.0.1:8080`, keeps a SQLite database under | 16 | It listens on `127.0.0.1:8080`, keeps a SQLite database under |
| 17 | -`~/.local/share/glean/`, and serves a plain HTML interface at the root — so | 17 | +`~/.local/share/pulseboard/`, and serves a plain HTML interface at the root — so |
| 18 | it is usable on its own, without the Flutter client. | 18 | it is usable on its own, without the Flutter client. |
| 19 | 19 | ||
| 20 | ``` | 20 | ``` |
| 21 | -cd app && flutter run --dart-define=GLEAN_BASE_URL=http://127.0.0.1:8080 | 21 | +cd app && flutter run --dart-define=PULSEBOARD_BASE_URL=http://127.0.0.1:8080 |
| 22 | ``` | 22 | ``` |
| 23 | 23 | ||
| 24 | ## Why it is split | 24 | ## Why it is split |
modified
spike/nim/feedparser_probe.nim +1 -1 | @@ -229,7 +229,7 @@ proc main() = | ||
| 229 | 229 | for url in feeds: |
| 230 | 230 | try: |
| 231 | 231 | let client = newHttpClient(timeout = 20_000, |
| 232 | - userAgent = "glean-nim-spike/0.1") | |
| 232 | + userAgent = "pulseboard-nim-spike/0.1") | |
| 233 | 233 | defer: client.close() |
| 234 | 234 | let body = client.getContent(url) |
| 235 | 235 | let r = parseFeed(body, url) |
| @@ -229,7 +229,7 @@ proc main() = | |||
| 229 | for url in feeds: | 229 | for url in feeds: |
| 230 | try: | 230 | try: |
| 231 | let client = newHttpClient(timeout = 20_000, | 231 | let client = newHttpClient(timeout = 20_000, |
| 232 | - userAgent = "glean-nim-spike/0.1") | 232 | + userAgent = "pulseboard-nim-spike/0.1") |
| 233 | defer: client.close() | 233 | defer: client.close() |
| 234 | let body = client.getContent(url) | 234 | let body = client.getContent(url) |
| 235 | let r = parseFeed(body, url) | 235 | let r = parseFeed(body, url) |
renamed
spike/nim/pulseboard.nim +19 -19 | similarity index 93% | ||
| rename from spike/nim/glean.nim | ||
| rename to spike/nim/pulseboard.nim | ||
| @@ -1,19 +1,19 @@ | ||
| 1 | 1 | ## A small RSS reader. |
| 2 | 2 | ## |
| 3 | -## nim c -r -d:ssl glean.nim | |
| 4 | -## GLEAN_DB=~/.local/share/glean/db GLEAN_PORT=8080 ./glean | |
| 3 | +## nim c -r -d:ssl pulseboard.nim | |
| 4 | +## PULSEBOARD_DB=~/.local/share/pulseboard/db PULSEBOARD_PORT=8080 ./pulseboard | |
| 5 | 5 | ## |
| 6 | 6 | ## Single user, no accounts. It runs on your machine and reads your feeds, |
| 7 | 7 | ## and that assumption is what keeps this a few hundred lines instead of a |
| 8 | 8 | ## few thousand: no sessions, no CSRF, no per-user scoping, no ATProto. |
| 9 | 9 | ## |
| 10 | -## GLEAN_TOKEN puts a shared secret in front of everything, which is what | |
| 10 | +## PULSEBOARD_TOKEN puts a shared secret in front of everything, which is what | |
| 11 | 11 | ## makes it safe anywhere but loopback -- `/feeds` and `/fetch-content` will |
| 12 | 12 | ## fetch any URL handed to them, so an open instance is an SSRF proxy as well |
| 13 | 13 | ## as someone else's feed list. Unset, there is no gate at all and the local |
| 14 | 14 | ## case stays exactly as simple as it was. |
| 15 | 15 | ## |
| 16 | -## GLEAN_WEB must point at the Flutter web build. The reader refuses to start | |
| 16 | +## PULSEBOARD_WEB must point at the Flutter web build. The reader refuses to start | |
| 17 | 17 | ## without it so deployments cannot silently serve a different client. |
| 18 | 18 | ## |
| 19 | 19 | ## The pieces underneath -- fetching, parsing, storage, search, full-text |
| @@ -21,7 +21,7 @@ | ||
| 21 | 21 | |
| 22 | 22 | import std/[asyncdispatch, asynchttpserver, httpclient, json, options, os, |
| 23 | 23 | strformat, strutils, times, uri] |
| 24 | -import reader/[articlestore, feedfetcher, feedparser, feedstore, gleandb, | |
| 24 | +import reader/[articlestore, feedfetcher, feedparser, feedstore, pulseboarddb, | |
| 25 | 25 | opml, scraper, sqlite] |
| 26 | 26 | |
| 27 | 27 | const |
| @@ -32,7 +32,7 @@ const | ||
| 32 | 32 | DefaultPort = 8080 |
| 33 | 33 | |
| 34 | 34 | type Reader = ref object |
| 35 | - db: GleanDb | |
| 35 | + db: PulseboardDb | |
| 36 | 36 | token: string ## empty disables the gate entirely |
| 37 | 37 | webRoot: string ## required Flutter web bundle |
| 38 | 38 | |
| @@ -57,10 +57,10 @@ proc presentedToken(req: Request, queryKey: string): string = | ||
| 57 | 57 | ## |
| 58 | 58 | ## Three ways because three callers: a native client sends a header, a |
| 59 | 59 | ## person opens a link, and the browser then has a cookie. |
| 60 | - let h = req.headers.getOrDefault("X-Glean-Token").string | |
| 60 | + let h = req.headers.getOrDefault("X-Pulseboard-Token").string | |
| 61 | 61 | if h.len > 0: return h |
| 62 | 62 | if queryKey.len > 0: return queryKey |
| 63 | - cookieValue(req.headers.getOrDefault("Cookie").string, "glean_token") | |
| 63 | + cookieValue(req.headers.getOrDefault("Cookie").string, "pulseboard_token") | |
| 64 | 64 | |
| 65 | 65 | proc jsonResponse(req: Request, code: HttpCode, node: JsonNode) {.async.} = |
| 66 | 66 | await req.respond(code, $node, newHttpHeaders({ |
| @@ -208,7 +208,7 @@ proc handle(r: Reader, req: Request) {.async.} = | ||
| 208 | 208 | if param("key").len > 0: |
| 209 | 209 | await req.respond(Http303, "", newHttpHeaders({ |
| 210 | 210 | "Location": "/", |
| 211 | - "Set-Cookie": "glean_token=" & r.token & | |
| 211 | + "Set-Cookie": "pulseboard_token=" & r.token & | |
| 212 | 212 | "; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000", |
| 213 | 213 | })) |
| 214 | 214 | return |
| @@ -233,7 +233,7 @@ proc handle(r: Reader, req: Request) {.async.} = | ||
| 233 | 233 | feeds.add OpmlFeed(url: s.feedUrl, title: s.feedTitle, category: s.category) |
| 234 | 234 | await req.respond(Http200, renderOpml(feeds), newHttpHeaders({ |
| 235 | 235 | "Content-Type": "application/xml; charset=utf-8", |
| 236 | - "Content-Disposition": "attachment; filename=glean-subscriptions.opml", | |
| 236 | + "Content-Disposition": "attachment; filename=pulseboard-subscriptions.opml", | |
| 237 | 237 | })) |
| 238 | 238 | |
| 239 | 239 | of "articles": |
| @@ -351,7 +351,7 @@ proc handle(r: Reader, req: Request) {.async.} = | ||
| 351 | 351 | return |
| 352 | 352 | try: |
| 353 | 353 | let client = newHttpClient(timeout = 20_000, |
| 354 | - userAgent = "glean/0.1") | |
| 354 | + userAgent = "pulseboard/0.1") | |
| 355 | 355 | defer: client.close() |
| 356 | 356 | let text = extractArticle(client.getContent(a.get.url)) |
| 357 | 357 | r.db.updateArticleFullContent(id, text) |
| @@ -372,25 +372,25 @@ proc handle(r: Reader, req: Request) {.async.} = | ||
| 372 | 372 | await fail(req, Http405, "method not allowed") |
| 373 | 373 | |
| 374 | 374 | proc main() {.async.} = |
| 375 | - let dbPath = getEnv("GLEAN_DB", getHomeDir() / ".local/share/glean/glean") | |
| 375 | + let dbPath = getEnv("PULSEBOARD_DB", getHomeDir() / ".local/share/pulseboard/pulseboard") | |
| 376 | 376 | createDir(dbPath.parentDir) |
| 377 | - let port = try: parseInt(getEnv("GLEAN_PORT", $DefaultPort)) | |
| 377 | + let port = try: parseInt(getEnv("PULSEBOARD_PORT", $DefaultPort)) | |
| 378 | 378 | except ValueError: DefaultPort |
| 379 | 379 | |
| 380 | - var db = gleandb.open(dbPath) | |
| 380 | + var db = pulseboarddb.open(dbPath) | |
| 381 | 381 | db.migrate() |
| 382 | 382 | db.db.run("INSERT OR IGNORE INTO users (did) VALUES (?)", p(LocalUser)) |
| 383 | 383 | |
| 384 | - let token = getEnv("GLEAN_TOKEN") | |
| 385 | - let webRoot = getEnv("GLEAN_WEB") | |
| 384 | + let token = getEnv("PULSEBOARD_TOKEN") | |
| 385 | + let webRoot = getEnv("PULSEBOARD_WEB") | |
| 386 | 386 | if webRoot.len == 0: |
| 387 | - quit("GLEAN_WEB is required and must point at a Flutter web build") | |
| 387 | + quit("PULSEBOARD_WEB is required and must point at a Flutter web build") | |
| 388 | 388 | if not dirExists(webRoot) or not fileExists(webRoot / "index.html"): |
| 389 | - quit(&"GLEAN_WEB points at {webRoot}, which has no index.html") | |
| 389 | + quit(&"PULSEBOARD_WEB points at {webRoot}, which has no index.html") | |
| 390 | 390 | |
| 391 | 391 | let reader = Reader(db: db, token: token, webRoot: webRoot) |
| 392 | 392 | |
| 393 | - echo &"glean reading from {dbPath}" | |
| 393 | + echo &"pulseboard reading from {dbPath}" | |
| 394 | 394 | if webRoot.len > 0: echo &"serving the client from {webRoot}" |
| 395 | 395 | if token.len > 0: |
| 396 | 396 | echo "token required: append ?key=… once to set the cookie" |
| similarity index 93% | |||
| rename from spike/nim/glean.nim | |||
| rename to spike/nim/pulseboard.nim | |||
| @@ -1,19 +1,19 @@ | |||
| 1 | ## A small RSS reader. | 1 | ## A small RSS reader. |
| 2 | ## | 2 | ## |
| 3 | -## nim c -r -d:ssl glean.nim | 3 | +## nim c -r -d:ssl pulseboard.nim |
| 4 | -## GLEAN_DB=~/.local/share/glean/db GLEAN_PORT=8080 ./glean | 4 | +## PULSEBOARD_DB=~/.local/share/pulseboard/db PULSEBOARD_PORT=8080 ./pulseboard |
| 5 | ## | 5 | ## |
| 6 | ## Single user, no accounts. It runs on your machine and reads your feeds, | 6 | ## Single user, no accounts. It runs on your machine and reads your feeds, |
| 7 | ## and that assumption is what keeps this a few hundred lines instead of a | 7 | ## and that assumption is what keeps this a few hundred lines instead of a |
| 8 | ## few thousand: no sessions, no CSRF, no per-user scoping, no ATProto. | 8 | ## few thousand: no sessions, no CSRF, no per-user scoping, no ATProto. |
| 9 | ## | 9 | ## |
| 10 | -## GLEAN_TOKEN puts a shared secret in front of everything, which is what | 10 | +## PULSEBOARD_TOKEN puts a shared secret in front of everything, which is what |
| 11 | ## makes it safe anywhere but loopback -- `/feeds` and `/fetch-content` will | 11 | ## makes it safe anywhere but loopback -- `/feeds` and `/fetch-content` will |
| 12 | ## fetch any URL handed to them, so an open instance is an SSRF proxy as well | 12 | ## fetch any URL handed to them, so an open instance is an SSRF proxy as well |
| 13 | ## as someone else's feed list. Unset, there is no gate at all and the local | 13 | ## as someone else's feed list. Unset, there is no gate at all and the local |
| 14 | ## case stays exactly as simple as it was. | 14 | ## case stays exactly as simple as it was. |
| 15 | ## | 15 | ## |
| 16 | -## GLEAN_WEB must point at the Flutter web build. The reader refuses to start | 16 | +## PULSEBOARD_WEB must point at the Flutter web build. The reader refuses to start |
| 17 | ## without it so deployments cannot silently serve a different client. | 17 | ## without it so deployments cannot silently serve a different client. |
| 18 | ## | 18 | ## |
| 19 | ## The pieces underneath -- fetching, parsing, storage, search, full-text | 19 | ## The pieces underneath -- fetching, parsing, storage, search, full-text |
| @@ -21,7 +21,7 @@ | |||
| 21 | 21 | ||
| 22 | import std/[asyncdispatch, asynchttpserver, httpclient, json, options, os, | 22 | import std/[asyncdispatch, asynchttpserver, httpclient, json, options, os, |
| 23 | strformat, strutils, times, uri] | 23 | strformat, strutils, times, uri] |
| 24 | -import reader/[articlestore, feedfetcher, feedparser, feedstore, gleandb, | 24 | +import reader/[articlestore, feedfetcher, feedparser, feedstore, pulseboarddb, |
| 25 | opml, scraper, sqlite] | 25 | opml, scraper, sqlite] |
| 26 | 26 | ||
| 27 | const | 27 | const |
| @@ -32,7 +32,7 @@ const | |||
| 32 | DefaultPort = 8080 | 32 | DefaultPort = 8080 |
| 33 | 33 | ||
| 34 | type Reader = ref object | 34 | type Reader = ref object |
| 35 | - db: GleanDb | 35 | + db: PulseboardDb |
| 36 | token: string ## empty disables the gate entirely | 36 | token: string ## empty disables the gate entirely |
| 37 | webRoot: string ## required Flutter web bundle | 37 | webRoot: string ## required Flutter web bundle |
| 38 | 38 | ||
| @@ -57,10 +57,10 @@ proc presentedToken(req: Request, queryKey: string): string = | |||
| 57 | ## | 57 | ## |
| 58 | ## Three ways because three callers: a native client sends a header, a | 58 | ## Three ways because three callers: a native client sends a header, a |
| 59 | ## person opens a link, and the browser then has a cookie. | 59 | ## person opens a link, and the browser then has a cookie. |
| 60 | - let h = req.headers.getOrDefault("X-Glean-Token").string | 60 | + let h = req.headers.getOrDefault("X-Pulseboard-Token").string |
| 61 | if h.len > 0: return h | 61 | if h.len > 0: return h |
| 62 | if queryKey.len > 0: return queryKey | 62 | if queryKey.len > 0: return queryKey |
| 63 | - cookieValue(req.headers.getOrDefault("Cookie").string, "glean_token") | 63 | + cookieValue(req.headers.getOrDefault("Cookie").string, "pulseboard_token") |
| 64 | 64 | ||
| 65 | proc jsonResponse(req: Request, code: HttpCode, node: JsonNode) {.async.} = | 65 | proc jsonResponse(req: Request, code: HttpCode, node: JsonNode) {.async.} = |
| 66 | await req.respond(code, $node, newHttpHeaders({ | 66 | await req.respond(code, $node, newHttpHeaders({ |
| @@ -208,7 +208,7 @@ proc handle(r: Reader, req: Request) {.async.} = | |||
| 208 | if param("key").len > 0: | 208 | if param("key").len > 0: |
| 209 | await req.respond(Http303, "", newHttpHeaders({ | 209 | await req.respond(Http303, "", newHttpHeaders({ |
| 210 | "Location": "/", | 210 | "Location": "/", |
| 211 | - "Set-Cookie": "glean_token=" & r.token & | 211 | + "Set-Cookie": "pulseboard_token=" & r.token & |
| 212 | "; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000", | 212 | "; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000", |
| 213 | })) | 213 | })) |
| 214 | return | 214 | return |
| @@ -233,7 +233,7 @@ proc handle(r: Reader, req: Request) {.async.} = | |||
| 233 | feeds.add OpmlFeed(url: s.feedUrl, title: s.feedTitle, category: s.category) | 233 | feeds.add OpmlFeed(url: s.feedUrl, title: s.feedTitle, category: s.category) |
| 234 | await req.respond(Http200, renderOpml(feeds), newHttpHeaders({ | 234 | await req.respond(Http200, renderOpml(feeds), newHttpHeaders({ |
| 235 | "Content-Type": "application/xml; charset=utf-8", | 235 | "Content-Type": "application/xml; charset=utf-8", |
| 236 | - "Content-Disposition": "attachment; filename=glean-subscriptions.opml", | 236 | + "Content-Disposition": "attachment; filename=pulseboard-subscriptions.opml", |
| 237 | })) | 237 | })) |
| 238 | 238 | ||
| 239 | of "articles": | 239 | of "articles": |
| @@ -351,7 +351,7 @@ proc handle(r: Reader, req: Request) {.async.} = | |||
| 351 | return | 351 | return |
| 352 | try: | 352 | try: |
| 353 | let client = newHttpClient(timeout = 20_000, | 353 | let client = newHttpClient(timeout = 20_000, |
| 354 | - userAgent = "glean/0.1") | 354 | + userAgent = "pulseboard/0.1") |
| 355 | defer: client.close() | 355 | defer: client.close() |
| 356 | let text = extractArticle(client.getContent(a.get.url)) | 356 | let text = extractArticle(client.getContent(a.get.url)) |
| 357 | r.db.updateArticleFullContent(id, text) | 357 | r.db.updateArticleFullContent(id, text) |
| @@ -372,25 +372,25 @@ proc handle(r: Reader, req: Request) {.async.} = | |||
| 372 | await fail(req, Http405, "method not allowed") | 372 | await fail(req, Http405, "method not allowed") |
| 373 | 373 | ||
| 374 | proc main() {.async.} = | 374 | proc main() {.async.} = |
| 375 | - let dbPath = getEnv("GLEAN_DB", getHomeDir() / ".local/share/glean/glean") | 375 | + let dbPath = getEnv("PULSEBOARD_DB", getHomeDir() / ".local/share/pulseboard/pulseboard") |
| 376 | createDir(dbPath.parentDir) | 376 | createDir(dbPath.parentDir) |
| 377 | - let port = try: parseInt(getEnv("GLEAN_PORT", $DefaultPort)) | 377 | + let port = try: parseInt(getEnv("PULSEBOARD_PORT", $DefaultPort)) |
| 378 | except ValueError: DefaultPort | 378 | except ValueError: DefaultPort |
| 379 | 379 | ||
| 380 | - var db = gleandb.open(dbPath) | 380 | + var db = pulseboarddb.open(dbPath) |
| 381 | db.migrate() | 381 | db.migrate() |
| 382 | db.db.run("INSERT OR IGNORE INTO users (did) VALUES (?)", p(LocalUser)) | 382 | db.db.run("INSERT OR IGNORE INTO users (did) VALUES (?)", p(LocalUser)) |
| 383 | 383 | ||
| 384 | - let token = getEnv("GLEAN_TOKEN") | 384 | + let token = getEnv("PULSEBOARD_TOKEN") |
| 385 | - let webRoot = getEnv("GLEAN_WEB") | 385 | + let webRoot = getEnv("PULSEBOARD_WEB") |
| 386 | if webRoot.len == 0: | 386 | if webRoot.len == 0: |
| 387 | - quit("GLEAN_WEB is required and must point at a Flutter web build") | 387 | + quit("PULSEBOARD_WEB is required and must point at a Flutter web build") |
| 388 | if not dirExists(webRoot) or not fileExists(webRoot / "index.html"): | 388 | if not dirExists(webRoot) or not fileExists(webRoot / "index.html"): |
| 389 | - quit(&"GLEAN_WEB points at {webRoot}, which has no index.html") | 389 | + quit(&"PULSEBOARD_WEB points at {webRoot}, which has no index.html") |
| 390 | 390 | ||
| 391 | let reader = Reader(db: db, token: token, webRoot: webRoot) | 391 | let reader = Reader(db: db, token: token, webRoot: webRoot) |
| 392 | 392 | ||
| 393 | - echo &"glean reading from {dbPath}" | 393 | + echo &"pulseboard reading from {dbPath}" |
| 394 | if webRoot.len > 0: echo &"serving the client from {webRoot}" | 394 | if webRoot.len > 0: echo &"serving the client from {webRoot}" |
| 395 | if token.len > 0: | 395 | if token.len > 0: |
| 396 | echo "token required: append ?key=… once to set the cookie" | 396 | echo "token required: append ?key=… once to set the cookie" |
renamed
spike/nim/pulseboard.nimble +3 -3 | similarity index 93% | ||
| rename from spike/nim/glean.nimble | ||
| rename to spike/nim/pulseboard.nimble | ||
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | version = "0.1.0" |
| 2 | -author = "glean" | |
| 3 | -description = "glean: a small RSS reader" | |
| 2 | +author = "pulseboard" | |
| 3 | +description = "pulseboard: a small RSS reader" | |
| 4 | 4 | license = "MIT" |
| 5 | 5 | srcDir = "." |
| 6 | 6 | |
| @@ -8,7 +8,7 @@ requires "nim >= 2.2.0" | ||
| 8 | 8 | |
| 9 | 9 | # Probes that touch only this machine. Safe to run anywhere, including CI. |
| 10 | 10 | const offlineProbes = @[ |
| 11 | - "gleandb_probe", | |
| 11 | + "pulseboarddb_probe", | |
| 12 | 12 | "stores_probe", |
| 13 | 13 | ] |
| 14 | 14 | |
| similarity index 93% | |||
| rename from spike/nim/glean.nimble | |||
| rename to spike/nim/pulseboard.nimble | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | version = "0.1.0" | 1 | version = "0.1.0" |
| 2 | -author = "glean" | 2 | +author = "pulseboard" |
| 3 | -description = "glean: a small RSS reader" | 3 | +description = "pulseboard: a small RSS reader" |
| 4 | license = "MIT" | 4 | license = "MIT" |
| 5 | srcDir = "." | 5 | srcDir = "." |
| 6 | 6 | ||
| @@ -8,7 +8,7 @@ requires "nim >= 2.2.0" | |||
| 8 | 8 | ||
| 9 | # Probes that touch only this machine. Safe to run anywhere, including CI. | 9 | # Probes that touch only this machine. Safe to run anywhere, including CI. |
| 10 | const offlineProbes = @[ | 10 | const offlineProbes = @[ |
| 11 | - "gleandb_probe", | 11 | + "pulseboarddb_probe", |
| 12 | "stores_probe", | 12 | "stores_probe", |
| 13 | ] | 13 | ] |
| 14 | 14 | ||
renamed
spike/nim/pulseboarddb_probe.nim +6 -6 | similarity index 94% | ||
| rename from spike/nim/gleandb_probe.nim | ||
| rename to spike/nim/pulseboarddb_probe.nim | ||
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | ## Checks the three-database layout, the schema, and the behaviour that |
| 2 | 2 | ## depends on both. |
| 3 | 3 | ## |
| 4 | -## nim c -r -d:ssl gleandb_probe.nim | |
| 4 | +## nim c -r -d:ssl pulseboarddb_probe.nim | |
| 5 | 5 | ## |
| 6 | 6 | ## Most of this is about things that fail quietly. An ATTACH that did not |
| 7 | 7 | ## happen, a pragma that did not take, an FTS index whose triggers never fire |
| @@ -9,7 +9,7 @@ | ||
| 9 | 9 | ## degrades or diverges later. |
| 10 | 10 | |
| 11 | 11 | import std/[options, os, strformat, strutils] |
| 12 | -import reader/[gleandb, schema, sqlite] | |
| 12 | +import reader/[pulseboarddb, schema, sqlite] | |
| 13 | 13 | |
| 14 | 14 | var failures = 0 |
| 15 | 15 | |
| @@ -25,14 +25,14 @@ proc cleanup(base: string) = | ||
| 25 | 25 | removeFile(base & suffix & ext) |
| 26 | 26 | |
| 27 | 27 | proc main() = |
| 28 | - echo "Glean database layer probe" | |
| 28 | + echo "Pulseboard database layer probe" | |
| 29 | 29 | echo "" |
| 30 | 30 | |
| 31 | - let base = getTempDir() / "glean_db_probe" | |
| 31 | + let base = getTempDir() / "pulseboard_db_probe" | |
| 32 | 32 | cleanup(base) |
| 33 | 33 | defer: cleanup(base) |
| 34 | 34 | |
| 35 | - var g = gleandb.open(base) | |
| 35 | + var g = pulseboarddb.open(base) | |
| 36 | 36 | defer: g.close() |
| 37 | 37 | |
| 38 | 38 | report("the schema covers the reader's tables", readerSchema.len == 15, |
| @@ -126,7 +126,7 @@ proc main() = | ||
| 126 | 126 | |
| 127 | 127 | echo "" |
| 128 | 128 | if failures == 0: |
| 129 | - echo "RESULT: the database layer matches Glean's layout and behaviour." | |
| 129 | + echo "RESULT: the database layer matches Pulseboard's layout and behaviour." | |
| 130 | 130 | else: |
| 131 | 131 | echo &"RESULT: {failures} check(s) failed." |
| 132 | 132 | quit 1 |
| similarity index 94% | |||
| rename from spike/nim/gleandb_probe.nim | |||
| rename to spike/nim/pulseboarddb_probe.nim | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | ## Checks the three-database layout, the schema, and the behaviour that | 1 | ## Checks the three-database layout, the schema, and the behaviour that |
| 2 | ## depends on both. | 2 | ## depends on both. |
| 3 | ## | 3 | ## |
| 4 | -## nim c -r -d:ssl gleandb_probe.nim | 4 | +## nim c -r -d:ssl pulseboarddb_probe.nim |
| 5 | ## | 5 | ## |
| 6 | ## Most of this is about things that fail quietly. An ATTACH that did not | 6 | ## Most of this is about things that fail quietly. An ATTACH that did not |
| 7 | ## happen, a pragma that did not take, an FTS index whose triggers never fire | 7 | ## happen, a pragma that did not take, an FTS index whose triggers never fire |
| @@ -9,7 +9,7 @@ | |||
| 9 | ## degrades or diverges later. | 9 | ## degrades or diverges later. |
| 10 | 10 | ||
| 11 | import std/[options, os, strformat, strutils] | 11 | import std/[options, os, strformat, strutils] |
| 12 | -import reader/[gleandb, schema, sqlite] | 12 | +import reader/[pulseboarddb, schema, sqlite] |
| 13 | 13 | ||
| 14 | var failures = 0 | 14 | var failures = 0 |
| 15 | 15 | ||
| @@ -25,14 +25,14 @@ proc cleanup(base: string) = | |||
| 25 | removeFile(base & suffix & ext) | 25 | removeFile(base & suffix & ext) |
| 26 | 26 | ||
| 27 | proc main() = | 27 | proc main() = |
| 28 | - echo "Glean database layer probe" | 28 | + echo "Pulseboard database layer probe" |
| 29 | echo "" | 29 | echo "" |
| 30 | 30 | ||
| 31 | - let base = getTempDir() / "glean_db_probe" | 31 | + let base = getTempDir() / "pulseboard_db_probe" |
| 32 | cleanup(base) | 32 | cleanup(base) |
| 33 | defer: cleanup(base) | 33 | defer: cleanup(base) |
| 34 | 34 | ||
| 35 | - var g = gleandb.open(base) | 35 | + var g = pulseboarddb.open(base) |
| 36 | defer: g.close() | 36 | defer: g.close() |
| 37 | 37 | ||
| 38 | report("the schema covers the reader's tables", readerSchema.len == 15, | 38 | report("the schema covers the reader's tables", readerSchema.len == 15, |
| @@ -126,7 +126,7 @@ proc main() = | |||
| 126 | 126 | ||
| 127 | echo "" | 127 | echo "" |
| 128 | if failures == 0: | 128 | if failures == 0: |
| 129 | - echo "RESULT: the database layer matches Glean's layout and behaviour." | 129 | + echo "RESULT: the database layer matches Pulseboard's layout and behaviour." |
| 130 | else: | 130 | else: |
| 131 | echo &"RESULT: {failures} check(s) failed." | 131 | echo &"RESULT: {failures} check(s) failed." |
| 132 | quit 1 | 132 | quit 1 |
modified
spike/nim/reader/articlestore.nim +14 -14 | @@ -7,7 +7,7 @@ | ||
| 7 | 7 | |
| 8 | 8 | import std/[options, strutils, times] |
| 9 | 9 | import ./feedstore |
| 10 | -import ./gleandb | |
| 10 | +import ./pulseboarddb | |
| 11 | 11 | import ./sqlite |
| 12 | 12 | |
| 13 | 13 | type |
| @@ -78,15 +78,15 @@ const articleFrom = """ | ||
| 78 | 78 | LEFT JOIN articles.feeds f ON f.feed_url = a.feed_url |
| 79 | 79 | LEFT JOIN articles.read_state r ON r.article_id = a.id AND r.user_did = ?""" |
| 80 | 80 | |
| 81 | -proc getArticle*(g: GleanDb, userDid: string, id: int64): Option[Article] = | |
| 81 | +proc getArticle*(g: PulseboardDb, userDid: string, id: int64): Option[Article] = | |
| 82 | 82 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.id = ?", |
| 83 | 83 | [p(userDid), p(id)], readArticleRow) |
| 84 | 84 | |
| 85 | -proc getArticleByUrl*(g: GleanDb, userDid, url: string): Option[Article] = | |
| 85 | +proc getArticleByUrl*(g: PulseboardDb, userDid, url: string): Option[Article] = | |
| 86 | 86 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.url = ?", |
| 87 | 87 | [p(userDid), p(url)], readArticleRow) |
| 88 | 88 | |
| 89 | -proc listArticles*(g: GleanDb, userDid: string, filter = afAll, | |
| 89 | +proc listArticles*(g: PulseboardDb, userDid: string, filter = afAll, | |
| 90 | 90 | feedUrl = "", category = "", sortOldest = false, |
| 91 | 91 | limit = 25, offset = 0): seq[Article] = |
| 92 | 92 | ## The main listing, scoped to what the user subscribes to. |
| @@ -120,7 +120,7 @@ proc listArticles*(g: GleanDb, userDid: string, filter = afAll, | ||
| 120 | 120 | |
| 121 | 121 | g.db.queryAll(sql, params, readArticleRow) |
| 122 | 122 | |
| 123 | -proc searchArticles*(g: GleanDb, userDid, query: string, | |
| 123 | +proc searchArticles*(g: PulseboardDb, userDid, query: string, | |
| 124 | 124 | limit = 25, offset = 0): seq[Article] = |
| 125 | 125 | ## Full-text search over the user's subscriptions. |
| 126 | 126 | ## |
| @@ -143,7 +143,7 @@ proc searchArticles*(g: GleanDb, userDid, query: string, | ||
| 143 | 143 | [p(userDid), p(userDid), p(query), |
| 144 | 144 | p(limit.int64), p(offset.int64)], readArticleRow) |
| 145 | 145 | |
| 146 | -proc batchUpsertArticles*(g: GleanDb, articles: seq[NewArticle], | |
| 146 | +proc batchUpsertArticles*(g: PulseboardDb, articles: seq[NewArticle], | |
| 147 | 147 | retentionDays = 0) = |
| 148 | 148 | ## Ingest a fetch. One transaction, one prepared statement. |
| 149 | 149 | ## |
| @@ -191,13 +191,13 @@ proc batchUpsertArticles*(g: GleanDb, articles: seq[NewArticle], | ||
| 191 | 191 | for url in feedUrls: |
| 192 | 192 | restore.exec(p(url)) |
| 193 | 193 | |
| 194 | -proc updateArticleFullContent*(g: GleanDb, id: int64, fullContent: string) = | |
| 194 | +proc updateArticleFullContent*(g: PulseboardDb, id: int64, fullContent: string) = | |
| 195 | 195 | g.db.run("UPDATE articles.articles SET full_content = ? WHERE id = ?", |
| 196 | 196 | pOrNull(fullContent), p(id)) |
| 197 | 197 | |
| 198 | 198 | # --- read state ------------------------------------------------------------ |
| 199 | 199 | |
| 200 | -proc setRead(g: GleanDb, userDid: string, articleId: int64, isRead: bool) = | |
| 200 | +proc setRead(g: PulseboardDb, userDid: string, articleId: int64, isRead: bool) = | |
| 201 | 201 | g.db.run("""INSERT INTO articles.read_state (user_did, article_id, is_read, read_at) |
| 202 | 202 | VALUES (?, ?, ?, CURRENT_TIMESTAMP) |
| 203 | 203 | ON CONFLICT(user_did, article_id) DO UPDATE SET |
| @@ -205,13 +205,13 @@ proc setRead(g: GleanDb, userDid: string, articleId: int64, isRead: bool) = | ||
| 205 | 205 | read_at = excluded.read_at""", |
| 206 | 206 | p(userDid), p(articleId), p(if isRead: 1'i64 else: 0'i64)) |
| 207 | 207 | |
| 208 | -proc markArticleRead*(g: GleanDb, userDid: string, articleId: int64) = | |
| 208 | +proc markArticleRead*(g: PulseboardDb, userDid: string, articleId: int64) = | |
| 209 | 209 | setRead(g, userDid, articleId, true) |
| 210 | 210 | |
| 211 | -proc markArticleUnread*(g: GleanDb, userDid: string, articleId: int64) = | |
| 211 | +proc markArticleUnread*(g: PulseboardDb, userDid: string, articleId: int64) = | |
| 212 | 212 | setRead(g, userDid, articleId, false) |
| 213 | 213 | |
| 214 | -proc markAllRead*(g: GleanDb, userDid, feedUrl: string) = | |
| 214 | +proc markAllRead*(g: PulseboardDb, userDid, feedUrl: string) = | |
| 215 | 215 | ## Scoped to one feed when given, otherwise everything the user subscribes |
| 216 | 216 | ## to. Getting this scope wrong is the difference between clearing a feed |
| 217 | 217 | ## and clearing the whole account, so the two cases are separate statements |
| @@ -234,7 +234,7 @@ proc markAllRead*(g: GleanDb, userDid, feedUrl: string) = | ||
| 234 | 234 | is_read = 1, read_at = CURRENT_TIMESTAMP""", |
| 235 | 235 | p(userDid), p(userDid)) |
| 236 | 236 | |
| 237 | -proc getUnreadCount*(g: GleanDb, userDid, feedUrl, category: string): int = | |
| 237 | +proc getUnreadCount*(g: PulseboardDb, userDid, feedUrl, category: string): int = | |
| 238 | 238 | var sql = """ |
| 239 | 239 | SELECT count(*) |
| 240 | 240 | FROM articles.articles a |
| @@ -252,12 +252,12 @@ proc getUnreadCount*(g: GleanDb, userDid, feedUrl, category: string): int = | ||
| 252 | 252 | params.add p(category) |
| 253 | 253 | g.db.queryInt(sql, params).get(0).int |
| 254 | 254 | |
| 255 | -proc isRead*(g: GleanDb, userDid: string, articleId: int64): bool = | |
| 255 | +proc isRead*(g: PulseboardDb, userDid: string, articleId: int64): bool = | |
| 256 | 256 | g.db.queryInt("""SELECT COALESCE(is_read, 0) FROM articles.read_state |
| 257 | 257 | WHERE user_did = ? AND article_id = ?""", |
| 258 | 258 | p(userDid), p(articleId)).get(0) != 0 |
| 259 | 259 | |
| 260 | -proc countNewArticles*(g: GleanDb, userDid: string, since: DateTime): int = | |
| 260 | +proc countNewArticles*(g: PulseboardDb, userDid: string, since: DateTime): int = | |
| 261 | 261 | g.db.queryInt(""" |
| 262 | 262 | SELECT count(*) FROM articles.articles a |
| 263 | 263 | JOIN articles.subscriptions s |
| @@ -7,7 +7,7 @@ | |||
| 7 | 7 | ||
| 8 | import std/[options, strutils, times] | 8 | import std/[options, strutils, times] |
| 9 | import ./feedstore | 9 | import ./feedstore |
| 10 | -import ./gleandb | 10 | +import ./pulseboarddb |
| 11 | import ./sqlite | 11 | import ./sqlite |
| 12 | 12 | ||
| 13 | type | 13 | type |
| @@ -78,15 +78,15 @@ const articleFrom = """ | |||
| 78 | LEFT JOIN articles.feeds f ON f.feed_url = a.feed_url | 78 | LEFT JOIN articles.feeds f ON f.feed_url = a.feed_url |
| 79 | LEFT JOIN articles.read_state r ON r.article_id = a.id AND r.user_did = ?""" | 79 | LEFT JOIN articles.read_state r ON r.article_id = a.id AND r.user_did = ?""" |
| 80 | 80 | ||
| 81 | -proc getArticle*(g: GleanDb, userDid: string, id: int64): Option[Article] = | 81 | +proc getArticle*(g: PulseboardDb, userDid: string, id: int64): Option[Article] = |
| 82 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.id = ?", | 82 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.id = ?", |
| 83 | [p(userDid), p(id)], readArticleRow) | 83 | [p(userDid), p(id)], readArticleRow) |
| 84 | 84 | ||
| 85 | -proc getArticleByUrl*(g: GleanDb, userDid, url: string): Option[Article] = | 85 | +proc getArticleByUrl*(g: PulseboardDb, userDid, url: string): Option[Article] = |
| 86 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.url = ?", | 86 | g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.url = ?", |
| 87 | [p(userDid), p(url)], readArticleRow) | 87 | [p(userDid), p(url)], readArticleRow) |
| 88 | 88 | ||
| 89 | -proc listArticles*(g: GleanDb, userDid: string, filter = afAll, | 89 | +proc listArticles*(g: PulseboardDb, userDid: string, filter = afAll, |
| 90 | feedUrl = "", category = "", sortOldest = false, | 90 | feedUrl = "", category = "", sortOldest = false, |
| 91 | limit = 25, offset = 0): seq[Article] = | 91 | limit = 25, offset = 0): seq[Article] = |
| 92 | ## The main listing, scoped to what the user subscribes to. | 92 | ## The main listing, scoped to what the user subscribes to. |
| @@ -120,7 +120,7 @@ proc listArticles*(g: GleanDb, userDid: string, filter = afAll, | |||
| 120 | 120 | ||
| 121 | g.db.queryAll(sql, params, readArticleRow) | 121 | g.db.queryAll(sql, params, readArticleRow) |
| 122 | 122 | ||
| 123 | -proc searchArticles*(g: GleanDb, userDid, query: string, | 123 | +proc searchArticles*(g: PulseboardDb, userDid, query: string, |
| 124 | limit = 25, offset = 0): seq[Article] = | 124 | limit = 25, offset = 0): seq[Article] = |
| 125 | ## Full-text search over the user's subscriptions. | 125 | ## Full-text search over the user's subscriptions. |
| 126 | ## | 126 | ## |
| @@ -143,7 +143,7 @@ proc searchArticles*(g: GleanDb, userDid, query: string, | |||
| 143 | [p(userDid), p(userDid), p(query), | 143 | [p(userDid), p(userDid), p(query), |
| 144 | p(limit.int64), p(offset.int64)], readArticleRow) | 144 | p(limit.int64), p(offset.int64)], readArticleRow) |
| 145 | 145 | ||
| 146 | -proc batchUpsertArticles*(g: GleanDb, articles: seq[NewArticle], | 146 | +proc batchUpsertArticles*(g: PulseboardDb, articles: seq[NewArticle], |
| 147 | retentionDays = 0) = | 147 | retentionDays = 0) = |
| 148 | ## Ingest a fetch. One transaction, one prepared statement. | 148 | ## Ingest a fetch. One transaction, one prepared statement. |
| 149 | ## | 149 | ## |
| @@ -191,13 +191,13 @@ proc batchUpsertArticles*(g: GleanDb, articles: seq[NewArticle], | |||
| 191 | for url in feedUrls: | 191 | for url in feedUrls: |
| 192 | restore.exec(p(url)) | 192 | restore.exec(p(url)) |
| 193 | 193 | ||
| 194 | -proc updateArticleFullContent*(g: GleanDb, id: int64, fullContent: string) = | 194 | +proc updateArticleFullContent*(g: PulseboardDb, id: int64, fullContent: string) = |
| 195 | g.db.run("UPDATE articles.articles SET full_content = ? WHERE id = ?", | 195 | g.db.run("UPDATE articles.articles SET full_content = ? WHERE id = ?", |
| 196 | pOrNull(fullContent), p(id)) | 196 | pOrNull(fullContent), p(id)) |
| 197 | 197 | ||
| 198 | # --- read state ------------------------------------------------------------ | 198 | # --- read state ------------------------------------------------------------ |
| 199 | 199 | ||
| 200 | -proc setRead(g: GleanDb, userDid: string, articleId: int64, isRead: bool) = | 200 | +proc setRead(g: PulseboardDb, userDid: string, articleId: int64, isRead: bool) = |
| 201 | g.db.run("""INSERT INTO articles.read_state (user_did, article_id, is_read, read_at) | 201 | g.db.run("""INSERT INTO articles.read_state (user_did, article_id, is_read, read_at) |
| 202 | VALUES (?, ?, ?, CURRENT_TIMESTAMP) | 202 | VALUES (?, ?, ?, CURRENT_TIMESTAMP) |
| 203 | ON CONFLICT(user_did, article_id) DO UPDATE SET | 203 | ON CONFLICT(user_did, article_id) DO UPDATE SET |
| @@ -205,13 +205,13 @@ proc setRead(g: GleanDb, userDid: string, articleId: int64, isRead: bool) = | |||
| 205 | read_at = excluded.read_at""", | 205 | read_at = excluded.read_at""", |
| 206 | p(userDid), p(articleId), p(if isRead: 1'i64 else: 0'i64)) | 206 | p(userDid), p(articleId), p(if isRead: 1'i64 else: 0'i64)) |
| 207 | 207 | ||
| 208 | -proc markArticleRead*(g: GleanDb, userDid: string, articleId: int64) = | 208 | +proc markArticleRead*(g: PulseboardDb, userDid: string, articleId: int64) = |
| 209 | setRead(g, userDid, articleId, true) | 209 | setRead(g, userDid, articleId, true) |
| 210 | 210 | ||
| 211 | -proc markArticleUnread*(g: GleanDb, userDid: string, articleId: int64) = | 211 | +proc markArticleUnread*(g: PulseboardDb, userDid: string, articleId: int64) = |
| 212 | setRead(g, userDid, articleId, false) | 212 | setRead(g, userDid, articleId, false) |
| 213 | 213 | ||
| 214 | -proc markAllRead*(g: GleanDb, userDid, feedUrl: string) = | 214 | +proc markAllRead*(g: PulseboardDb, userDid, feedUrl: string) = |
| 215 | ## Scoped to one feed when given, otherwise everything the user subscribes | 215 | ## Scoped to one feed when given, otherwise everything the user subscribes |
| 216 | ## to. Getting this scope wrong is the difference between clearing a feed | 216 | ## to. Getting this scope wrong is the difference between clearing a feed |
| 217 | ## and clearing the whole account, so the two cases are separate statements | 217 | ## and clearing the whole account, so the two cases are separate statements |
| @@ -234,7 +234,7 @@ proc markAllRead*(g: GleanDb, userDid, feedUrl: string) = | |||
| 234 | is_read = 1, read_at = CURRENT_TIMESTAMP""", | 234 | is_read = 1, read_at = CURRENT_TIMESTAMP""", |
| 235 | p(userDid), p(userDid)) | 235 | p(userDid), p(userDid)) |
| 236 | 236 | ||
| 237 | -proc getUnreadCount*(g: GleanDb, userDid, feedUrl, category: string): int = | 237 | +proc getUnreadCount*(g: PulseboardDb, userDid, feedUrl, category: string): int = |
| 238 | var sql = """ | 238 | var sql = """ |
| 239 | SELECT count(*) | 239 | SELECT count(*) |
| 240 | FROM articles.articles a | 240 | FROM articles.articles a |
| @@ -252,12 +252,12 @@ proc getUnreadCount*(g: GleanDb, userDid, feedUrl, category: string): int = | |||
| 252 | params.add p(category) | 252 | params.add p(category) |
| 253 | g.db.queryInt(sql, params).get(0).int | 253 | g.db.queryInt(sql, params).get(0).int |
| 254 | 254 | ||
| 255 | -proc isRead*(g: GleanDb, userDid: string, articleId: int64): bool = | 255 | +proc isRead*(g: PulseboardDb, userDid: string, articleId: int64): bool = |
| 256 | g.db.queryInt("""SELECT COALESCE(is_read, 0) FROM articles.read_state | 256 | g.db.queryInt("""SELECT COALESCE(is_read, 0) FROM articles.read_state |
| 257 | WHERE user_did = ? AND article_id = ?""", | 257 | WHERE user_did = ? AND article_id = ?""", |
| 258 | p(userDid), p(articleId)).get(0) != 0 | 258 | p(userDid), p(articleId)).get(0) != 0 |
| 259 | 259 | ||
| 260 | -proc countNewArticles*(g: GleanDb, userDid: string, since: DateTime): int = | 260 | +proc countNewArticles*(g: PulseboardDb, userDid: string, since: DateTime): int = |
| 261 | g.db.queryInt(""" | 261 | g.db.queryInt(""" |
| 262 | SELECT count(*) FROM articles.articles a | 262 | SELECT count(*) FROM articles.articles a |
| 263 | JOIN articles.subscriptions s | 263 | JOIN articles.subscriptions s |
modified
spike/nim/reader/feedfetcher.nim +1 -1 | @@ -48,7 +48,7 @@ type | ||
| 48 | 48 | const |
| 49 | 49 | AcceptFeed* = "application/xml,application/atom+xml,application/rss+xml," & |
| 50 | 50 | "application/rdf+xml,application/feed+json,text/html;q=0.9" |
| 51 | - DefaultUserAgent* = "glean-nim/0.1" | |
| 51 | + DefaultUserAgent* = "pulseboard-nim/0.1" | |
| 52 | 52 | |
| 53 | 53 | proc defaultConfig*(): FetchConfig = |
| 54 | 54 | FetchConfig( |
| @@ -48,7 +48,7 @@ type | |||
| 48 | const | 48 | const |
| 49 | AcceptFeed* = "application/xml,application/atom+xml,application/rss+xml," & | 49 | AcceptFeed* = "application/xml,application/atom+xml,application/rss+xml," & |
| 50 | "application/rdf+xml,application/feed+json,text/html;q=0.9" | 50 | "application/rdf+xml,application/feed+json,text/html;q=0.9" |
| 51 | - DefaultUserAgent* = "glean-nim/0.1" | 51 | + DefaultUserAgent* = "pulseboard-nim/0.1" |
| 52 | 52 | ||
| 53 | proc defaultConfig*(): FetchConfig = | 53 | proc defaultConfig*(): FetchConfig = |
| 54 | FetchConfig( | 54 | FetchConfig( |
modified
spike/nim/reader/feedstore.nim +17 -17 | @@ -6,7 +6,7 @@ | ||
| 6 | 6 | ## read on every listing and written only when someone subscribes. |
| 7 | 7 | |
| 8 | 8 | import std/[options, times] |
| 9 | -import ./gleandb | |
| 9 | +import ./pulseboarddb | |
| 10 | 10 | import ./sqlite |
| 11 | 11 | |
| 12 | 12 | type |
| @@ -73,7 +73,7 @@ const feedColumns = """f.feed_url, f.title, f.site_url, f.description, | ||
| 73 | 73 | f.feed_type, f.favicon_url, f.subscriber_count, f.error_count, f.last_error, |
| 74 | 74 | f.last_fetched_at""" |
| 75 | 75 | |
| 76 | -proc upsertFeed*(g: GleanDb, f: Feed) = | |
| 76 | +proc upsertFeed*(g: PulseboardDb, f: Feed) = | |
| 77 | 77 | ## Insert, or refresh the metadata of an existing feed. |
| 78 | 78 | ## |
| 79 | 79 | ## subscriber_count and the error columns are deliberately not touched: they |
| @@ -91,12 +91,12 @@ proc upsertFeed*(g: GleanDb, f: Feed) = | ||
| 91 | 91 | p(f.feedUrl), pOrNull(f.title), pOrNull(f.siteUrl), |
| 92 | 92 | pOrNull(f.description), pOrNull(f.feedType), pOrNull(f.faviconUrl)) |
| 93 | 93 | |
| 94 | -proc getFeed*(g: GleanDb, feedUrl: string): Option[Feed] = | |
| 94 | +proc getFeed*(g: PulseboardDb, feedUrl: string): Option[Feed] = | |
| 95 | 95 | g.db.queryFirst("SELECT " & feedColumns & |
| 96 | 96 | " FROM articles.feeds f WHERE f.feed_url = ?", |
| 97 | 97 | [p(feedUrl)], readFeed) |
| 98 | 98 | |
| 99 | -proc getFeedsToFetch*(g: GleanDb, olderThan: Duration, limit: int): seq[Feed] = | |
| 99 | +proc getFeedsToFetch*(g: PulseboardDb, olderThan: Duration, limit: int): seq[Feed] = | |
| 100 | 100 | ## Feeds due for a refresh: never fetched, or last fetched before the |
| 101 | 101 | ## cutoff. Dead feeds are excluded by error_count so a permanently broken |
| 102 | 102 | ## URL does not consume a fetch slot every cycle. |
| @@ -110,7 +110,7 @@ proc getFeedsToFetch*(g: GleanDb, olderThan: Duration, limit: int): seq[Feed] = | ||
| 110 | 110 | LIMIT ?""", |
| 111 | 111 | [p(cutoff), p(limit.int64)], readFeed) |
| 112 | 112 | |
| 113 | -proc markFeedFetched*(g: GleanDb, feedUrl: string) = | |
| 113 | +proc markFeedFetched*(g: PulseboardDb, feedUrl: string) = | |
| 114 | 114 | ## A successful fetch clears the error state, so a feed that recovers |
| 115 | 115 | ## re-enters the rotation instead of staying dead. |
| 116 | 116 | g.db.run("""UPDATE articles.feeds |
| @@ -119,14 +119,14 @@ proc markFeedFetched*(g: GleanDb, feedUrl: string) = | ||
| 119 | 119 | error_count = 0 |
| 120 | 120 | WHERE feed_url = ?""", p(feedUrl)) |
| 121 | 121 | |
| 122 | -proc markFeedFetchError*(g: GleanDb, feedUrl, lastError: string) = | |
| 122 | +proc markFeedFetchError*(g: PulseboardDb, feedUrl, lastError: string) = | |
| 123 | 123 | g.db.run("""UPDATE articles.feeds |
| 124 | 124 | SET last_fetched_at = CURRENT_TIMESTAMP, |
| 125 | 125 | last_error = ?, |
| 126 | 126 | error_count = error_count + 1 |
| 127 | 127 | WHERE feed_url = ?""", p(lastError), p(feedUrl)) |
| 128 | 128 | |
| 129 | -proc updateFeedFavicon*(g: GleanDb, feedUrl, faviconUrl: string) = | |
| 129 | +proc updateFeedFavicon*(g: PulseboardDb, feedUrl, faviconUrl: string) = | |
| 130 | 130 | g.db.run("UPDATE articles.feeds SET favicon_url = ? WHERE feed_url = ?", |
| 131 | 131 | pOrNull(faviconUrl), p(feedUrl)) |
| 132 | 132 | |
| @@ -146,7 +146,7 @@ proc readSubscription(r: Row): Subscription = | ||
| 146 | 146 | faviconUrl: r.str(9), |
| 147 | 147 | ) |
| 148 | 148 | |
| 149 | -proc createSubscription*(g: GleanDb, userDid, feedUrl, title, category, | |
| 149 | +proc createSubscription*(g: PulseboardDb, userDid, feedUrl, title, category, | |
| 150 | 150 | uri, cid: string) = |
| 151 | 151 | ## Subscribe, and keep subscriber_count in step. |
| 152 | 152 | ## |
| @@ -172,7 +172,7 @@ proc createSubscription*(g: GleanDb, userDid, feedUrl, title, category, | ||
| 172 | 172 | WHERE feed_url = ?) |
| 173 | 173 | WHERE feed_url = ?""", p(feedUrl), p(feedUrl)) |
| 174 | 174 | |
| 175 | -proc deleteSubscription*(g: GleanDb, userDid, feedUrl: string) = | |
| 175 | +proc deleteSubscription*(g: PulseboardDb, userDid, feedUrl: string) = | |
| 176 | 176 | g.db.transaction: |
| 177 | 177 | g.db.run("""DELETE FROM articles.subscriptions |
| 178 | 178 | WHERE user_did = ? AND feed_url = ?""", p(userDid), p(feedUrl)) |
| @@ -182,14 +182,14 @@ proc deleteSubscription*(g: GleanDb, userDid, feedUrl: string) = | ||
| 182 | 182 | WHERE feed_url = ?) |
| 183 | 183 | WHERE feed_url = ?""", p(feedUrl), p(feedUrl)) |
| 184 | 184 | |
| 185 | -proc deleteAllSubscriptions*(g: GleanDb, userDid: string) = | |
| 185 | +proc deleteAllSubscriptions*(g: PulseboardDb, userDid: string) = | |
| 186 | 186 | g.db.transaction: |
| 187 | 187 | g.db.run("DELETE FROM articles.subscriptions WHERE user_did = ?", p(userDid)) |
| 188 | 188 | g.db.run("""UPDATE articles.feeds SET subscriber_count = ( |
| 189 | 189 | SELECT count(*) FROM articles.subscriptions s |
| 190 | 190 | WHERE s.feed_url = articles.feeds.feed_url)""") |
| 191 | 191 | |
| 192 | -proc getSubscription*(g: GleanDb, userDid, feedUrl: string): Option[Subscription] = | |
| 192 | +proc getSubscription*(g: PulseboardDb, userDid, feedUrl: string): Option[Subscription] = | |
| 193 | 193 | g.db.queryFirst(""" |
| 194 | 194 | SELECT s.id, s.user_did, s.feed_url, |
| 195 | 195 | COALESCE(s.title, f.title, s.feed_url), s.category, s.uri, s.cid, |
| @@ -199,7 +199,7 @@ proc getSubscription*(g: GleanDb, userDid, feedUrl: string): Option[Subscription | ||
| 199 | 199 | WHERE s.user_did = ? AND s.feed_url = ?""", |
| 200 | 200 | [p(userDid), p(feedUrl)], readSubscription) |
| 201 | 201 | |
| 202 | -proc listSubscriptions*(g: GleanDb, userDid, category: string, | |
| 202 | +proc listSubscriptions*(g: PulseboardDb, userDid, category: string, | |
| 203 | 203 | limit, offset: int): seq[Subscription] = |
| 204 | 204 | ## The unread count is computed per row here rather than stored: it is |
| 205 | 205 | ## per-user and changes constantly, so a cached column would be wrong more |
| @@ -225,18 +225,18 @@ proc listSubscriptions*(g: GleanDb, userDid, category: string, | ||
| 225 | 225 | params.add p(offset.int64) |
| 226 | 226 | g.db.queryAll(sql, params, readSubscription) |
| 227 | 227 | |
| 228 | -proc getSubscriptionCount*(g: GleanDb, userDid: string): int = | |
| 228 | +proc getSubscriptionCount*(g: PulseboardDb, userDid: string): int = | |
| 229 | 229 | g.db.queryInt("SELECT count(*) FROM articles.subscriptions WHERE user_did = ?", |
| 230 | 230 | p(userDid)).get(0).int |
| 231 | 231 | |
| 232 | -proc getCategories*(g: GleanDb, userDid: string): seq[string] = | |
| 232 | +proc getCategories*(g: PulseboardDb, userDid: string): seq[string] = | |
| 233 | 233 | for r in g.db.query("""SELECT DISTINCT category FROM articles.subscriptions |
| 234 | 234 | WHERE user_did = ? AND category IS NOT NULL |
| 235 | 235 | AND category != '' |
| 236 | 236 | ORDER BY category COLLATE NOCASE""", p(userDid)): |
| 237 | 237 | result.add r.str(0) |
| 238 | 238 | |
| 239 | -proc listDeadFeeds*(g: GleanDb, userDid: string, threshold: int): seq[Feed] = | |
| 239 | +proc listDeadFeeds*(g: PulseboardDb, userDid: string, threshold: int): seq[Feed] = | |
| 240 | 240 | ## Subscribed feeds that keep failing, so the UI can offer a retry rather |
| 241 | 241 | ## than silently showing nothing. |
| 242 | 242 | g.db.queryAll("""SELECT """ & feedColumns & """ |
| @@ -246,14 +246,14 @@ proc listDeadFeeds*(g: GleanDb, userDid: string, threshold: int): seq[Feed] = | ||
| 246 | 246 | ORDER BY f.error_count DESC""", |
| 247 | 247 | [p(userDid), p(threshold.int64)], readFeed) |
| 248 | 248 | |
| 249 | -proc recountSubscriberCounts*(g: GleanDb) = | |
| 249 | +proc recountSubscriberCounts*(g: PulseboardDb) = | |
| 250 | 250 | ## Repair pass. The counts are maintained incrementally, so anything that |
| 251 | 251 | ## interrupts a subscribe leaves them drifted. |
| 252 | 252 | g.db.run("""UPDATE articles.feeds SET subscriber_count = ( |
| 253 | 253 | SELECT count(*) FROM articles.subscriptions s |
| 254 | 254 | WHERE s.feed_url = articles.feeds.feed_url)""") |
| 255 | 255 | |
| 256 | -proc batchUpsertFeeds*(g: GleanDb, feeds: seq[Feed]) = | |
| 256 | +proc batchUpsertFeeds*(g: PulseboardDb, feeds: seq[Feed]) = | |
| 257 | 257 | if feeds.len == 0: return |
| 258 | 258 | g.db.transaction: |
| 259 | 259 | var ps = g.db.prepared(""" |
| @@ -6,7 +6,7 @@ | |||
| 6 | ## read on every listing and written only when someone subscribes. | 6 | ## read on every listing and written only when someone subscribes. |
| 7 | 7 | ||
| 8 | import std/[options, times] | 8 | import std/[options, times] |
| 9 | -import ./gleandb | 9 | +import ./pulseboarddb |
| 10 | import ./sqlite | 10 | import ./sqlite |
| 11 | 11 | ||
| 12 | type | 12 | type |
| @@ -73,7 +73,7 @@ const feedColumns = """f.feed_url, f.title, f.site_url, f.description, | |||
| 73 | f.feed_type, f.favicon_url, f.subscriber_count, f.error_count, f.last_error, | 73 | f.feed_type, f.favicon_url, f.subscriber_count, f.error_count, f.last_error, |
| 74 | f.last_fetched_at""" | 74 | f.last_fetched_at""" |
| 75 | 75 | ||
| 76 | -proc upsertFeed*(g: GleanDb, f: Feed) = | 76 | +proc upsertFeed*(g: PulseboardDb, f: Feed) = |
| 77 | ## Insert, or refresh the metadata of an existing feed. | 77 | ## Insert, or refresh the metadata of an existing feed. |
| 78 | ## | 78 | ## |
| 79 | ## subscriber_count and the error columns are deliberately not touched: they | 79 | ## subscriber_count and the error columns are deliberately not touched: they |
| @@ -91,12 +91,12 @@ proc upsertFeed*(g: GleanDb, f: Feed) = | |||
| 91 | p(f.feedUrl), pOrNull(f.title), pOrNull(f.siteUrl), | 91 | p(f.feedUrl), pOrNull(f.title), pOrNull(f.siteUrl), |
| 92 | pOrNull(f.description), pOrNull(f.feedType), pOrNull(f.faviconUrl)) | 92 | pOrNull(f.description), pOrNull(f.feedType), pOrNull(f.faviconUrl)) |
| 93 | 93 | ||
| 94 | -proc getFeed*(g: GleanDb, feedUrl: string): Option[Feed] = | 94 | +proc getFeed*(g: PulseboardDb, feedUrl: string): Option[Feed] = |
| 95 | g.db.queryFirst("SELECT " & feedColumns & | 95 | g.db.queryFirst("SELECT " & feedColumns & |
| 96 | " FROM articles.feeds f WHERE f.feed_url = ?", | 96 | " FROM articles.feeds f WHERE f.feed_url = ?", |
| 97 | [p(feedUrl)], readFeed) | 97 | [p(feedUrl)], readFeed) |
| 98 | 98 | ||
| 99 | -proc getFeedsToFetch*(g: GleanDb, olderThan: Duration, limit: int): seq[Feed] = | 99 | +proc getFeedsToFetch*(g: PulseboardDb, olderThan: Duration, limit: int): seq[Feed] = |
| 100 | ## Feeds due for a refresh: never fetched, or last fetched before the | 100 | ## Feeds due for a refresh: never fetched, or last fetched before the |
| 101 | ## cutoff. Dead feeds are excluded by error_count so a permanently broken | 101 | ## cutoff. Dead feeds are excluded by error_count so a permanently broken |
| 102 | ## URL does not consume a fetch slot every cycle. | 102 | ## URL does not consume a fetch slot every cycle. |
| @@ -110,7 +110,7 @@ proc getFeedsToFetch*(g: GleanDb, olderThan: Duration, limit: int): seq[Feed] = | |||
| 110 | LIMIT ?""", | 110 | LIMIT ?""", |
| 111 | [p(cutoff), p(limit.int64)], readFeed) | 111 | [p(cutoff), p(limit.int64)], readFeed) |
| 112 | 112 | ||
| 113 | -proc markFeedFetched*(g: GleanDb, feedUrl: string) = | 113 | +proc markFeedFetched*(g: PulseboardDb, feedUrl: string) = |
| 114 | ## A successful fetch clears the error state, so a feed that recovers | 114 | ## A successful fetch clears the error state, so a feed that recovers |
| 115 | ## re-enters the rotation instead of staying dead. | 115 | ## re-enters the rotation instead of staying dead. |
| 116 | g.db.run("""UPDATE articles.feeds | 116 | g.db.run("""UPDATE articles.feeds |
| @@ -119,14 +119,14 @@ proc markFeedFetched*(g: GleanDb, feedUrl: string) = | |||
| 119 | error_count = 0 | 119 | error_count = 0 |
| 120 | WHERE feed_url = ?""", p(feedUrl)) | 120 | WHERE feed_url = ?""", p(feedUrl)) |
| 121 | 121 | ||
| 122 | -proc markFeedFetchError*(g: GleanDb, feedUrl, lastError: string) = | 122 | +proc markFeedFetchError*(g: PulseboardDb, feedUrl, lastError: string) = |
| 123 | g.db.run("""UPDATE articles.feeds | 123 | g.db.run("""UPDATE articles.feeds |
| 124 | SET last_fetched_at = CURRENT_TIMESTAMP, | 124 | SET last_fetched_at = CURRENT_TIMESTAMP, |
| 125 | last_error = ?, | 125 | last_error = ?, |
| 126 | error_count = error_count + 1 | 126 | error_count = error_count + 1 |
| 127 | WHERE feed_url = ?""", p(lastError), p(feedUrl)) | 127 | WHERE feed_url = ?""", p(lastError), p(feedUrl)) |
| 128 | 128 | ||
| 129 | -proc updateFeedFavicon*(g: GleanDb, feedUrl, faviconUrl: string) = | 129 | +proc updateFeedFavicon*(g: PulseboardDb, feedUrl, faviconUrl: string) = |
| 130 | g.db.run("UPDATE articles.feeds SET favicon_url = ? WHERE feed_url = ?", | 130 | g.db.run("UPDATE articles.feeds SET favicon_url = ? WHERE feed_url = ?", |
| 131 | pOrNull(faviconUrl), p(feedUrl)) | 131 | pOrNull(faviconUrl), p(feedUrl)) |
| 132 | 132 | ||
| @@ -146,7 +146,7 @@ proc readSubscription(r: Row): Subscription = | |||
| 146 | faviconUrl: r.str(9), | 146 | faviconUrl: r.str(9), |
| 147 | ) | 147 | ) |
| 148 | 148 | ||
| 149 | -proc createSubscription*(g: GleanDb, userDid, feedUrl, title, category, | 149 | +proc createSubscription*(g: PulseboardDb, userDid, feedUrl, title, category, |
| 150 | uri, cid: string) = | 150 | uri, cid: string) = |
| 151 | ## Subscribe, and keep subscriber_count in step. | 151 | ## Subscribe, and keep subscriber_count in step. |
| 152 | ## | 152 | ## |
| @@ -172,7 +172,7 @@ proc createSubscription*(g: GleanDb, userDid, feedUrl, title, category, | |||
| 172 | WHERE feed_url = ?) | 172 | WHERE feed_url = ?) |
| 173 | WHERE feed_url = ?""", p(feedUrl), p(feedUrl)) | 173 | WHERE feed_url = ?""", p(feedUrl), p(feedUrl)) |
| 174 | 174 | ||
| 175 | -proc deleteSubscription*(g: GleanDb, userDid, feedUrl: string) = | 175 | +proc deleteSubscription*(g: PulseboardDb, userDid, feedUrl: string) = |
| 176 | g.db.transaction: | 176 | g.db.transaction: |
| 177 | g.db.run("""DELETE FROM articles.subscriptions | 177 | g.db.run("""DELETE FROM articles.subscriptions |
| 178 | WHERE user_did = ? AND feed_url = ?""", p(userDid), p(feedUrl)) | 178 | WHERE user_did = ? AND feed_url = ?""", p(userDid), p(feedUrl)) |
| @@ -182,14 +182,14 @@ proc deleteSubscription*(g: GleanDb, userDid, feedUrl: string) = | |||
| 182 | WHERE feed_url = ?) | 182 | WHERE feed_url = ?) |
| 183 | WHERE feed_url = ?""", p(feedUrl), p(feedUrl)) | 183 | WHERE feed_url = ?""", p(feedUrl), p(feedUrl)) |
| 184 | 184 | ||
| 185 | -proc deleteAllSubscriptions*(g: GleanDb, userDid: string) = | 185 | +proc deleteAllSubscriptions*(g: PulseboardDb, userDid: string) = |
| 186 | g.db.transaction: | 186 | g.db.transaction: |
| 187 | g.db.run("DELETE FROM articles.subscriptions WHERE user_did = ?", p(userDid)) | 187 | g.db.run("DELETE FROM articles.subscriptions WHERE user_did = ?", p(userDid)) |
| 188 | g.db.run("""UPDATE articles.feeds SET subscriber_count = ( | 188 | g.db.run("""UPDATE articles.feeds SET subscriber_count = ( |
| 189 | SELECT count(*) FROM articles.subscriptions s | 189 | SELECT count(*) FROM articles.subscriptions s |
| 190 | WHERE s.feed_url = articles.feeds.feed_url)""") | 190 | WHERE s.feed_url = articles.feeds.feed_url)""") |
| 191 | 191 | ||
| 192 | -proc getSubscription*(g: GleanDb, userDid, feedUrl: string): Option[Subscription] = | 192 | +proc getSubscription*(g: PulseboardDb, userDid, feedUrl: string): Option[Subscription] = |
| 193 | g.db.queryFirst(""" | 193 | g.db.queryFirst(""" |
| 194 | SELECT s.id, s.user_did, s.feed_url, | 194 | SELECT s.id, s.user_did, s.feed_url, |
| 195 | COALESCE(s.title, f.title, s.feed_url), s.category, s.uri, s.cid, | 195 | COALESCE(s.title, f.title, s.feed_url), s.category, s.uri, s.cid, |
| @@ -199,7 +199,7 @@ proc getSubscription*(g: GleanDb, userDid, feedUrl: string): Option[Subscription | |||
| 199 | WHERE s.user_did = ? AND s.feed_url = ?""", | 199 | WHERE s.user_did = ? AND s.feed_url = ?""", |
| 200 | [p(userDid), p(feedUrl)], readSubscription) | 200 | [p(userDid), p(feedUrl)], readSubscription) |
| 201 | 201 | ||
| 202 | -proc listSubscriptions*(g: GleanDb, userDid, category: string, | 202 | +proc listSubscriptions*(g: PulseboardDb, userDid, category: string, |
| 203 | limit, offset: int): seq[Subscription] = | 203 | limit, offset: int): seq[Subscription] = |
| 204 | ## The unread count is computed per row here rather than stored: it is | 204 | ## The unread count is computed per row here rather than stored: it is |
| 205 | ## per-user and changes constantly, so a cached column would be wrong more | 205 | ## per-user and changes constantly, so a cached column would be wrong more |
| @@ -225,18 +225,18 @@ proc listSubscriptions*(g: GleanDb, userDid, category: string, | |||
| 225 | params.add p(offset.int64) | 225 | params.add p(offset.int64) |
| 226 | g.db.queryAll(sql, params, readSubscription) | 226 | g.db.queryAll(sql, params, readSubscription) |
| 227 | 227 | ||
| 228 | -proc getSubscriptionCount*(g: GleanDb, userDid: string): int = | 228 | +proc getSubscriptionCount*(g: PulseboardDb, userDid: string): int = |
| 229 | g.db.queryInt("SELECT count(*) FROM articles.subscriptions WHERE user_did = ?", | 229 | g.db.queryInt("SELECT count(*) FROM articles.subscriptions WHERE user_did = ?", |
| 230 | p(userDid)).get(0).int | 230 | p(userDid)).get(0).int |
| 231 | 231 | ||
| 232 | -proc getCategories*(g: GleanDb, userDid: string): seq[string] = | 232 | +proc getCategories*(g: PulseboardDb, userDid: string): seq[string] = |
| 233 | for r in g.db.query("""SELECT DISTINCT category FROM articles.subscriptions | 233 | for r in g.db.query("""SELECT DISTINCT category FROM articles.subscriptions |
| 234 | WHERE user_did = ? AND category IS NOT NULL | 234 | WHERE user_did = ? AND category IS NOT NULL |
| 235 | AND category != '' | 235 | AND category != '' |
| 236 | ORDER BY category COLLATE NOCASE""", p(userDid)): | 236 | ORDER BY category COLLATE NOCASE""", p(userDid)): |
| 237 | result.add r.str(0) | 237 | result.add r.str(0) |
| 238 | 238 | ||
| 239 | -proc listDeadFeeds*(g: GleanDb, userDid: string, threshold: int): seq[Feed] = | 239 | +proc listDeadFeeds*(g: PulseboardDb, userDid: string, threshold: int): seq[Feed] = |
| 240 | ## Subscribed feeds that keep failing, so the UI can offer a retry rather | 240 | ## Subscribed feeds that keep failing, so the UI can offer a retry rather |
| 241 | ## than silently showing nothing. | 241 | ## than silently showing nothing. |
| 242 | g.db.queryAll("""SELECT """ & feedColumns & """ | 242 | g.db.queryAll("""SELECT """ & feedColumns & """ |
| @@ -246,14 +246,14 @@ proc listDeadFeeds*(g: GleanDb, userDid: string, threshold: int): seq[Feed] = | |||
| 246 | ORDER BY f.error_count DESC""", | 246 | ORDER BY f.error_count DESC""", |
| 247 | [p(userDid), p(threshold.int64)], readFeed) | 247 | [p(userDid), p(threshold.int64)], readFeed) |
| 248 | 248 | ||
| 249 | -proc recountSubscriberCounts*(g: GleanDb) = | 249 | +proc recountSubscriberCounts*(g: PulseboardDb) = |
| 250 | ## Repair pass. The counts are maintained incrementally, so anything that | 250 | ## Repair pass. The counts are maintained incrementally, so anything that |
| 251 | ## interrupts a subscribe leaves them drifted. | 251 | ## interrupts a subscribe leaves them drifted. |
| 252 | g.db.run("""UPDATE articles.feeds SET subscriber_count = ( | 252 | g.db.run("""UPDATE articles.feeds SET subscriber_count = ( |
| 253 | SELECT count(*) FROM articles.subscriptions s | 253 | SELECT count(*) FROM articles.subscriptions s |
| 254 | WHERE s.feed_url = articles.feeds.feed_url)""") | 254 | WHERE s.feed_url = articles.feeds.feed_url)""") |
| 255 | 255 | ||
| 256 | -proc batchUpsertFeeds*(g: GleanDb, feeds: seq[Feed]) = | 256 | +proc batchUpsertFeeds*(g: PulseboardDb, feeds: seq[Feed]) = |
| 257 | if feeds.len == 0: return | 257 | if feeds.len == 0: return |
| 258 | g.db.transaction: | 258 | g.db.transaction: |
| 259 | var ps = g.db.prepared(""" | 259 | var ps = g.db.prepared(""" |
modified
spike/nim/reader/opml.nim +1 -1 | @@ -40,7 +40,7 @@ proc parseOpml*(source: string): seq[OpmlFeed] = | ||
| 40 | 40 | raise newException(ValueError, "expected an OPML document") |
| 41 | 41 | collect(root, "", result) |
| 42 | 42 | |
| 43 | -proc renderOpml*(feeds: openArray[OpmlFeed], title = "Glean subscriptions"): string = | |
| 43 | +proc renderOpml*(feeds: openArray[OpmlFeed], title = "Pulseboard subscriptions"): string = | |
| 44 | 44 | result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<opml version=\"2.0\">\n <head><title>" & |
| 45 | 45 | escapeXml(title) & "</title></head>\n <body>\n" |
| 46 | 46 | var uncategorized: seq[OpmlFeed] |
| @@ -40,7 +40,7 @@ proc parseOpml*(source: string): seq[OpmlFeed] = | |||
| 40 | raise newException(ValueError, "expected an OPML document") | 40 | raise newException(ValueError, "expected an OPML document") |
| 41 | collect(root, "", result) | 41 | collect(root, "", result) |
| 42 | 42 | ||
| 43 | -proc renderOpml*(feeds: openArray[OpmlFeed], title = "Glean subscriptions"): string = | 43 | +proc renderOpml*(feeds: openArray[OpmlFeed], title = "Pulseboard subscriptions"): string = |
| 44 | result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<opml version=\"2.0\">\n <head><title>" & | 44 | result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<opml version=\"2.0\">\n <head><title>" & |
| 45 | escapeXml(title) & "</title></head>\n <body>\n" | 45 | escapeXml(title) & "</title></head>\n <body>\n" |
| 46 | var uncategorized: seq[OpmlFeed] | 46 | var uncategorized: seq[OpmlFeed] |
renamed
spike/nim/reader/pulseboarddb.nim +7 -7 | similarity index 89% | ||
| rename from spike/nim/reader/gleandb.nim | ||
| rename to spike/nim/reader/pulseboarddb.nim | ||
| @@ -15,7 +15,7 @@ import ./schema | ||
| 15 | 15 | import ./sqlite |
| 16 | 16 | |
| 17 | 17 | type |
| 18 | - GleanDb* = object | |
| 18 | + PulseboardDb* = object | |
| 19 | 19 | db*: Db |
| 20 | 20 | basePath*: string |
| 21 | 21 | |
| @@ -51,7 +51,7 @@ proc applyPragmas(db: Db, schemaName: string, pragmas: openArray[string]) = | ||
| 51 | 51 | except SqliteError: |
| 52 | 52 | discard |
| 53 | 53 | |
| 54 | -proc open*(basePath: string): GleanDb = | |
| 54 | +proc open*(basePath: string): PulseboardDb = | |
| 55 | 55 | ## Open `<basePath>_users` and attach `_articles`. |
| 56 | 56 | let |
| 57 | 57 | usersPath = basePath & "_users" |
| @@ -66,21 +66,21 @@ proc open*(basePath: string): GleanDb = | ||
| 66 | 66 | db.exec &"ATTACH DATABASE '{path.replace(\"'\", \"''\")}' AS {name}" |
| 67 | 67 | applyPragmas(db, name, AttachedPragmas) |
| 68 | 68 | |
| 69 | - GleanDb(db: db, basePath: basePath) | |
| 69 | + PulseboardDb(db: db, basePath: basePath) | |
| 70 | 70 | |
| 71 | -proc migrate*(g: GleanDb) = | |
| 71 | +proc migrate*(g: PulseboardDb) = | |
| 72 | 72 | ## Apply the schema. Every statement is IF NOT EXISTS, so this runs on every |
| 73 | 73 | ## boot rather than being gated on a version. |
| 74 | 74 | for stmt in readerSchema: g.db.exec stmt |
| 75 | 75 | |
| 76 | -proc close*(g: var GleanDb) = | |
| 76 | +proc close*(g: var PulseboardDb) = | |
| 77 | 77 | g.db.close() |
| 78 | 78 | |
| 79 | -proc journalMode*(g: GleanDb, schemaName = ""): string = | |
| 79 | +proc journalMode*(g: PulseboardDb, schemaName = ""): string = | |
| 80 | 80 | let sql = if schemaName.len == 0: "PRAGMA journal_mode" |
| 81 | 81 | else: &"PRAGMA {schemaName}.journal_mode" |
| 82 | 82 | g.db.queryText(sql).get("") |
| 83 | 83 | |
| 84 | -proc attachedSchemas*(g: GleanDb): seq[string] = | |
| 84 | +proc attachedSchemas*(g: PulseboardDb): seq[string] = | |
| 85 | 85 | for row in g.db.rows("PRAGMA database_list"): |
| 86 | 86 | result.add row[1] |
| similarity index 89% | |||
| rename from spike/nim/reader/gleandb.nim | |||
| rename to spike/nim/reader/pulseboarddb.nim | |||
| @@ -15,7 +15,7 @@ import ./schema | |||
| 15 | import ./sqlite | 15 | import ./sqlite |
| 16 | 16 | ||
| 17 | type | 17 | type |
| 18 | - GleanDb* = object | 18 | + PulseboardDb* = object |
| 19 | db*: Db | 19 | db*: Db |
| 20 | basePath*: string | 20 | basePath*: string |
| 21 | 21 | ||
| @@ -51,7 +51,7 @@ proc applyPragmas(db: Db, schemaName: string, pragmas: openArray[string]) = | |||
| 51 | except SqliteError: | 51 | except SqliteError: |
| 52 | discard | 52 | discard |
| 53 | 53 | ||
| 54 | -proc open*(basePath: string): GleanDb = | 54 | +proc open*(basePath: string): PulseboardDb = |
| 55 | ## Open `<basePath>_users` and attach `_articles`. | 55 | ## Open `<basePath>_users` and attach `_articles`. |
| 56 | let | 56 | let |
| 57 | usersPath = basePath & "_users" | 57 | usersPath = basePath & "_users" |
| @@ -66,21 +66,21 @@ proc open*(basePath: string): GleanDb = | |||
| 66 | db.exec &"ATTACH DATABASE '{path.replace(\"'\", \"''\")}' AS {name}" | 66 | db.exec &"ATTACH DATABASE '{path.replace(\"'\", \"''\")}' AS {name}" |
| 67 | applyPragmas(db, name, AttachedPragmas) | 67 | applyPragmas(db, name, AttachedPragmas) |
| 68 | 68 | ||
| 69 | - GleanDb(db: db, basePath: basePath) | 69 | + PulseboardDb(db: db, basePath: basePath) |
| 70 | 70 | ||
| 71 | -proc migrate*(g: GleanDb) = | 71 | +proc migrate*(g: PulseboardDb) = |
| 72 | ## Apply the schema. Every statement is IF NOT EXISTS, so this runs on every | 72 | ## Apply the schema. Every statement is IF NOT EXISTS, so this runs on every |
| 73 | ## boot rather than being gated on a version. | 73 | ## boot rather than being gated on a version. |
| 74 | for stmt in readerSchema: g.db.exec stmt | 74 | for stmt in readerSchema: g.db.exec stmt |
| 75 | 75 | ||
| 76 | -proc close*(g: var GleanDb) = | 76 | +proc close*(g: var PulseboardDb) = |
| 77 | g.db.close() | 77 | g.db.close() |
| 78 | 78 | ||
| 79 | -proc journalMode*(g: GleanDb, schemaName = ""): string = | 79 | +proc journalMode*(g: PulseboardDb, schemaName = ""): string = |
| 80 | let sql = if schemaName.len == 0: "PRAGMA journal_mode" | 80 | let sql = if schemaName.len == 0: "PRAGMA journal_mode" |
| 81 | else: &"PRAGMA {schemaName}.journal_mode" | 81 | else: &"PRAGMA {schemaName}.journal_mode" |
| 82 | g.db.queryText(sql).get("") | 82 | g.db.queryText(sql).get("") |
| 83 | 83 | ||
| 84 | -proc attachedSchemas*(g: GleanDb): seq[string] = | 84 | +proc attachedSchemas*(g: PulseboardDb): seq[string] = |
| 85 | for row in g.db.rows("PRAGMA database_list"): | 85 | for row in g.db.rows("PRAGMA database_list"): |
| 86 | result.add row[1] | 86 | result.add row[1] |
modified
spike/nim/reader/sqlite.nim +1 -1 | @@ -172,7 +172,7 @@ proc isNull*(r: Row, col: int): bool = | ||
| 172 | 172 | sqlite3_column_type(r.stmt, col.cint) == SQLITE_NULL |
| 173 | 173 | |
| 174 | 174 | proc str*(r: Row, col: int): string = |
| 175 | - ## NULL reads as "". Most of Glean's text columns are nullable but mean the | |
| 175 | + ## NULL reads as "". Most of Pulseboard's text columns are nullable but mean the | |
| 176 | 176 | ## empty string, and the Go side collapses them the same way at the wire |
| 177 | 177 | ## boundary (see nullStr in internal/server/api.go). |
| 178 | 178 | if r.isNull(col): return "" |
| @@ -172,7 +172,7 @@ proc isNull*(r: Row, col: int): bool = | |||
| 172 | sqlite3_column_type(r.stmt, col.cint) == SQLITE_NULL | 172 | sqlite3_column_type(r.stmt, col.cint) == SQLITE_NULL |
| 173 | 173 | ||
| 174 | proc str*(r: Row, col: int): string = | 174 | proc str*(r: Row, col: int): string = |
| 175 | - ## NULL reads as "". Most of Glean's text columns are nullable but mean the | 175 | + ## NULL reads as "". Most of Pulseboard's text columns are nullable but mean the |
| 176 | ## empty string, and the Go side collapses them the same way at the wire | 176 | ## empty string, and the Go side collapses them the same way at the wire |
| 177 | ## boundary (see nullStr in internal/server/api.go). | 177 | ## boundary (see nullStr in internal/server/api.go). |
| 178 | if r.isNull(col): return "" | 178 | if r.isNull(col): return "" |
modified
spike/nim/readme.md +5 -5 | @@ -1,14 +1,14 @@ | ||
| 1 | -# glean — a small RSS reader | |
| 1 | +# pulseboard — a small RSS reader | |
| 2 | 2 | |
| 3 | 3 | One binary, one SQLite database, no accounts. |
| 4 | 4 | |
| 5 | 5 | ``` |
| 6 | -nim c -d:ssl glean.nim | |
| 7 | -./glean | |
| 6 | +nim c -d:ssl pulseboard.nim | |
| 7 | +./pulseboard | |
| 8 | 8 | ``` |
| 9 | 9 | |
| 10 | 10 | Then open <http://127.0.0.1:8080>. The database lives under |
| 11 | -`~/.local/share/glean/`; override with `GLEAN_PORT` and `GLEAN_DB`. | |
| 11 | +`~/.local/share/pulseboard/`; override with `PULSEBOARD_PORT` and `PULSEBOARD_DB`. | |
| 12 | 12 | |
| 13 | 13 | ## The interface |
| 14 | 14 | |
| @@ -77,7 +77,7 @@ Six modules under `reader/`: | ||
| 77 | 77 | | `feedparser` | RSS 2.0, RDF, Atom and JSON Feed | |
| 78 | 78 | | `scraper` | readability-style extraction, output sanitised | |
| 79 | 79 | | `sqlite` | a thin wrapper: prepare, bind, step, transactions | |
| 80 | -| `gleandb` | two attached databases and the schema | | |
| 80 | +| `pulseboarddb` | two attached databases and the schema | | |
| 81 | 81 | | `feedstore` / `articlestore` | the queries | |
| 82 | 82 | |
| 83 | 83 | Each carries its own checks: |
| @@ -1,14 +1,14 @@ | |||
| 1 | -# glean — a small RSS reader | 1 | +# pulseboard — a small RSS reader |
| 2 | 2 | ||
| 3 | One binary, one SQLite database, no accounts. | 3 | One binary, one SQLite database, no accounts. |
| 4 | 4 | ||
| 5 | ``` | 5 | ``` |
| 6 | -nim c -d:ssl glean.nim | 6 | +nim c -d:ssl pulseboard.nim |
| 7 | -./glean | 7 | +./pulseboard |
| 8 | ``` | 8 | ``` |
| 9 | 9 | ||
| 10 | Then open <http://127.0.0.1:8080>. The database lives under | 10 | Then open <http://127.0.0.1:8080>. The database lives under |
| 11 | -`~/.local/share/glean/`; override with `GLEAN_PORT` and `GLEAN_DB`. | 11 | +`~/.local/share/pulseboard/`; override with `PULSEBOARD_PORT` and `PULSEBOARD_DB`. |
| 12 | 12 | ||
| 13 | ## The interface | 13 | ## The interface |
| 14 | 14 | ||
| @@ -77,7 +77,7 @@ Six modules under `reader/`: | |||
| 77 | | `feedparser` | RSS 2.0, RDF, Atom and JSON Feed | | 77 | | `feedparser` | RSS 2.0, RDF, Atom and JSON Feed | |
| 78 | | `scraper` | readability-style extraction, output sanitised | | 78 | | `scraper` | readability-style extraction, output sanitised | |
| 79 | | `sqlite` | a thin wrapper: prepare, bind, step, transactions | | 79 | | `sqlite` | a thin wrapper: prepare, bind, step, transactions | |
| 80 | -| `gleandb` | two attached databases and the schema | | 80 | +| `pulseboarddb` | two attached databases and the schema | |
| 81 | | `feedstore` / `articlestore` | the queries | | 81 | | `feedstore` / `articlestore` | the queries | |
| 82 | 82 | ||
| 83 | Each carries its own checks: | 83 | Each carries its own checks: |
modified
spike/nim/scraper_probe.nim +1 -1 | @@ -197,7 +197,7 @@ proc main() = | ||
| 197 | 197 | block: |
| 198 | 198 | try: |
| 199 | 199 | let client = newHttpClient(timeout = 20_000, |
| 200 | - userAgent = "glean-nim-spike/0.1") | |
| 200 | + userAgent = "pulseboard-nim-spike/0.1") | |
| 201 | 201 | defer: client.close() |
| 202 | 202 | let feed = parseFeed(client.getContent("https://blog.rust-lang.org/feed.xml"), |
| 203 | 203 | "https://blog.rust-lang.org/feed.xml") |
| @@ -197,7 +197,7 @@ proc main() = | |||
| 197 | block: | 197 | block: |
| 198 | try: | 198 | try: |
| 199 | let client = newHttpClient(timeout = 20_000, | 199 | let client = newHttpClient(timeout = 20_000, |
| 200 | - userAgent = "glean-nim-spike/0.1") | 200 | + userAgent = "pulseboard-nim-spike/0.1") |
| 201 | defer: client.close() | 201 | defer: client.close() |
| 202 | let feed = parseFeed(client.getContent("https://blog.rust-lang.org/feed.xml"), | 202 | let feed = parseFeed(client.getContent("https://blog.rust-lang.org/feed.xml"), |
| 203 | "https://blog.rust-lang.org/feed.xml") | 203 | "https://blog.rust-lang.org/feed.xml") |
modified
spike/nim/stores_probe.nim +3 -3 | @@ -7,7 +7,7 @@ | ||
| 7 | 7 | ## returns other people's feeds, a NULL that sorts to the top. |
| 8 | 8 | |
| 9 | 9 | import std/[options, os, strformat, strutils, times] |
| 10 | -import reader/[articlestore, feedstore, gleandb, sqlite] | |
| 10 | +import reader/[articlestore, feedstore, pulseboarddb, sqlite] | |
| 11 | 11 | |
| 12 | 12 | var failures = 0 |
| 13 | 13 | |
| @@ -32,11 +32,11 @@ proc main() = | ||
| 32 | 32 | echo "Article / feed store probe" |
| 33 | 33 | echo "" |
| 34 | 34 | |
| 35 | - let base = getTempDir() / "glean_stores_probe" | |
| 35 | + let base = getTempDir() / "pulseboard_stores_probe" | |
| 36 | 36 | cleanup(base) |
| 37 | 37 | defer: cleanup(base) |
| 38 | 38 | |
| 39 | - var g = gleandb.open(base) | |
| 39 | + var g = pulseboarddb.open(base) | |
| 40 | 40 | defer: g.close() |
| 41 | 41 | g.migrate() |
| 42 | 42 | |
| @@ -7,7 +7,7 @@ | |||
| 7 | ## returns other people's feeds, a NULL that sorts to the top. | 7 | ## returns other people's feeds, a NULL that sorts to the top. |
| 8 | 8 | ||
| 9 | import std/[options, os, strformat, strutils, times] | 9 | import std/[options, os, strformat, strutils, times] |
| 10 | -import reader/[articlestore, feedstore, gleandb, sqlite] | 10 | +import reader/[articlestore, feedstore, pulseboarddb, sqlite] |
| 11 | 11 | ||
| 12 | var failures = 0 | 12 | var failures = 0 |
| 13 | 13 | ||
| @@ -32,11 +32,11 @@ proc main() = | |||
| 32 | echo "Article / feed store probe" | 32 | echo "Article / feed store probe" |
| 33 | echo "" | 33 | echo "" |
| 34 | 34 | ||
| 35 | - let base = getTempDir() / "glean_stores_probe" | 35 | + let base = getTempDir() / "pulseboard_stores_probe" |
| 36 | cleanup(base) | 36 | cleanup(base) |
| 37 | defer: cleanup(base) | 37 | defer: cleanup(base) |
| 38 | 38 | ||
| 39 | - var g = gleandb.open(base) | 39 | + var g = pulseboarddb.open(base) |
| 40 | defer: g.close() | 40 | defer: g.close() |
| 41 | g.migrate() | 41 | g.migrate() |
| 42 | 42 | ||