diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 1d451208..a5ee31f9 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -11,8 +11,8 @@ version = "1.0" repositories { google() } dependencies { - implementation(project(":common")) - implementation("androidx.activity:activity-compose:1.3.0-alpha08") + implementation(projects.common) + implementation("androidx.activity:activity-compose:1.3.0-beta01") } android { diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 00000000..5e2bd406 --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,23 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") + id("com.google.devtools.ksp") version "1.5.10-1.0.0-beta01" +} + +dependencies { + api(libs.thirdparty.retrofit.lib) + ksp(libs.thirdparty.moshix.ksp) + implementation(libs.thirdparty.moshi.lib) + implementation(libs.thirdparty.retrofit.moshiConverter) { exclude(group = "com.squareup.moshi") } + testImplementation(libs.kotlin.coroutines.core) + testImplementation(libs.testing.kotlintest.junit) + testImplementation(libs.testing.mockWebServer) +} + +tasks.withType { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + languageVersion = "1.5" + } +} diff --git a/api/src/main/kotlin/dev/msfjarvis/lobsters/data/api/LobstersApi.kt b/api/src/main/kotlin/dev/msfjarvis/lobsters/data/api/LobstersApi.kt new file mode 100644 index 00000000..8d7047fc --- /dev/null +++ b/api/src/main/kotlin/dev/msfjarvis/lobsters/data/api/LobstersApi.kt @@ -0,0 +1,22 @@ +package dev.msfjarvis.lobsters.data.api + +import dev.msfjarvis.lobsters.model.LobstersPost +import dev.msfjarvis.lobsters.model.LobstersPostDetails +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +/** Simple interface defining an API for lobste.rs */ +interface LobstersApi { + + @GET("hottest.json") suspend fun getHottestPosts(@Query("page") page: Int): List + + @GET("newest.json") suspend fun getNewestPosts(@Query("page") page: Int): List + + @GET("s/{postId}.json") + suspend fun getPostDetails(@Path("postId") postId: String): LobstersPostDetails + + companion object { + const val BASE_URL = "https://lobste.rs" + } +} diff --git a/api/src/main/kotlin/dev/msfjarvis/lobsters/model/Comment.kt b/api/src/main/kotlin/dev/msfjarvis/lobsters/model/Comment.kt new file mode 100644 index 00000000..431788b3 --- /dev/null +++ b/api/src/main/kotlin/dev/msfjarvis/lobsters/model/Comment.kt @@ -0,0 +1,20 @@ +package dev.msfjarvis.lobsters.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +class Comment( + @Json(name = "short_id") val shortId: String, + @Json(name = "short_id_url") val shortIdUrl: String, + @Json(name = "created_at") val createdAt: String, + @Json(name = "updated_at") val updatedAt: String, + @Json(name = "is_deleted") val isDeleted: Boolean, + @Json(name = "is_moderated") val isModerated: Boolean, + val score: Long, + val flags: Long, + val comment: String, + val url: String, + @Json(name = "indent_level") val indentLevel: Long, + @Json(name = "commenting_user") val user: User, +) diff --git a/api/src/main/kotlin/dev/msfjarvis/lobsters/model/KeybaseSignature.kt b/api/src/main/kotlin/dev/msfjarvis/lobsters/model/KeybaseSignature.kt new file mode 100644 index 00000000..b90df662 --- /dev/null +++ b/api/src/main/kotlin/dev/msfjarvis/lobsters/model/KeybaseSignature.kt @@ -0,0 +1,10 @@ +package dev.msfjarvis.lobsters.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +class KeybaseSignature( + @Json(name = "kb_username") val kbUsername: String, + @Json(name = "sig_hash") val sigHash: String, +) diff --git a/api/src/main/kotlin/dev/msfjarvis/lobsters/model/LobstersPost.kt b/api/src/main/kotlin/dev/msfjarvis/lobsters/model/LobstersPost.kt new file mode 100644 index 00000000..7c34f859 --- /dev/null +++ b/api/src/main/kotlin/dev/msfjarvis/lobsters/model/LobstersPost.kt @@ -0,0 +1,20 @@ +package dev.msfjarvis.lobsters.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +class LobstersPost( + @Json(name = "short_id") val shortId: String, + @Json(name = "short_id_url") val shortIdUrl: String, + @Json(name = "created_at") val createdAt: String, + val title: String, + val url: String, + val score: Long, + val flags: Long, + @Json(name = "comment_count") val commentCount: Long, + val description: String, + @Json(name = "comments_url") val commentsUrl: String, + @Json(name = "submitter_user") val submitter: User, + val tags: List, +) diff --git a/api/src/main/kotlin/dev/msfjarvis/lobsters/model/LobstersPostDetails.kt b/api/src/main/kotlin/dev/msfjarvis/lobsters/model/LobstersPostDetails.kt new file mode 100644 index 00000000..de9dfbb6 --- /dev/null +++ b/api/src/main/kotlin/dev/msfjarvis/lobsters/model/LobstersPostDetails.kt @@ -0,0 +1,21 @@ +package dev.msfjarvis.lobsters.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +class LobstersPostDetails( + @Json(name = "short_id") val shortId: String, + @Json(name = "short_id_url") val shortIdUrl: String, + @Json(name = "created_at") val createdAt: String, + val title: String, + val url: String, + val score: Long, + val flags: Long, + @Json(name = "comment_count") val commentCount: Long, + val description: String, + @Json(name = "comments_url") val commentsUrl: String, + @Json(name = "submitter_user") val submitter: User, + val tags: List, + val comments: List, +) diff --git a/api/src/main/kotlin/dev/msfjarvis/lobsters/model/User.kt b/api/src/main/kotlin/dev/msfjarvis/lobsters/model/User.kt new file mode 100644 index 00000000..4073c85b --- /dev/null +++ b/api/src/main/kotlin/dev/msfjarvis/lobsters/model/User.kt @@ -0,0 +1,19 @@ +package dev.msfjarvis.lobsters.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +class User( + val username: String, + @Json(name = "created_at") val createdAt: String, + @Json(name = "is_admin") val isAdmin: Boolean, + val about: String, + @Json(name = "is_moderator") val isModerator: Boolean, + val karma: Long = 0, + @Json(name = "avatar_url") val avatarUrl: String, + @Json(name = "invited_by_user") val invitedByUser: String, + @Json(name = "github_username") val githubUsername: String? = null, + @Json(name = "twitter_username") val twitterUsername: String? = null, + @Json(name = "keybase_signatures") val keybaseSignatures: List = emptyList(), +) diff --git a/api/src/test/kotlin/dev/msfjarvis/lobsters/data/api/LobstersApiTest.kt b/api/src/test/kotlin/dev/msfjarvis/lobsters/data/api/LobstersApiTest.kt new file mode 100644 index 00000000..0fb01ee7 --- /dev/null +++ b/api/src/test/kotlin/dev/msfjarvis/lobsters/data/api/LobstersApiTest.kt @@ -0,0 +1,89 @@ +package dev.msfjarvis.lobsters.data.api + +import com.squareup.moshi.Moshi +import dev.msfjarvis.lobsters.util.TestUtils +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail +import kotlinx.coroutines.runBlocking +import mockwebserver3.Dispatcher +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.RecordedRequest +import okhttp3.OkHttpClient +import org.junit.AfterClass +import org.junit.BeforeClass +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.create + +class LobstersApiTest { + + companion object { + private val webServer = MockWebServer() + private val moshi = Moshi.Builder().build() + private val okHttp = OkHttpClient.Builder().build() + private val retrofit = + Retrofit.Builder() + .client(okHttp) + .baseUrl("http://localhost:8080/") + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + private val apiClient = retrofit.create() + + @JvmStatic + @BeforeClass + fun setUp() { + webServer.start(8080) + webServer.dispatcher = + object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + val path = requireNotNull(request.path) + return when { + path.startsWith("/hottest") -> + MockResponse().setBody(TestUtils.getJson("hottest.json")).setResponseCode(200) + path.startsWith("/s/") -> + MockResponse() + .setBody(TestUtils.getJson("post_details_d9ucpe.json")) + .setResponseCode(200) + else -> fail("'$path' unexpected") + } + } + } + } + + @JvmStatic + @AfterClass + fun tearDown() { + webServer.shutdown() + } + } + + @Test + fun `api gets correct number of items`() = runBlocking { + val posts = apiClient.getHottestPosts(1) + assertEquals(25, posts.size) + } + + @Test + fun `no moderator posts in test data`() = runBlocking { + val posts = apiClient.getHottestPosts(1) + val moderatorPosts = posts.asSequence().filter { it.submitter.isModerator }.toSet() + assertTrue(moderatorPosts.isEmpty()) + } + + @Test + fun `posts with no urls`() = runBlocking { + val posts = apiClient.getHottestPosts(1) + val commentsOnlyPosts = posts.asSequence().filter { it.url.isEmpty() }.toSet() + assertEquals(2, commentsOnlyPosts.size) + } + + @Test + fun `post details with comments`() = runBlocking { + val postDetails = apiClient.getPostDetails("d9ucpe") + assertEquals(7, postDetails.commentCount) + assertEquals(7, postDetails.comments.size) + } +} diff --git a/api/src/test/kotlin/dev/msfjarvis/lobsters/util/TestUtils.kt b/api/src/test/kotlin/dev/msfjarvis/lobsters/util/TestUtils.kt new file mode 100644 index 00000000..b64d4556 --- /dev/null +++ b/api/src/test/kotlin/dev/msfjarvis/lobsters/util/TestUtils.kt @@ -0,0 +1,12 @@ +package dev.msfjarvis.lobsters.util + +import java.io.File + +object TestUtils { + fun getJson(path: String): String { + // Load the JSON response + val uri = javaClass.classLoader.getResource(path) + val file = File(uri.path) + return String(file.readBytes()) + } +} diff --git a/api/src/test/resources/hottest.json b/api/src/test/resources/hottest.json new file mode 100644 index 00000000..4e3f86a6 --- /dev/null +++ b/api/src/test/resources/hottest.json @@ -0,0 +1 @@ +[{"short_id":"q1hh1g","short_id_url":"https://lobste.rs/s/q1hh1g","created_at":"2020-09-21T08:04:24.000-05:00","title":"Simple Anomaly Detection Using Plain SQL","url":"https://hakibenita.com/sql-anomaly-detection","score":33,"flags":0,"comment_count":5,"description":"","comments_url":"https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql","submitter_user":{"username":"Haki","created_at":"2019-01-04T01:25:42.000-06:00","is_admin":false,"about":"https://hakibenita.com","is_moderator":false,"karma":278,"avatar_url":"/avatars/Haki-100.png","invited_by_user":"pstef"},"tags":["databases"]},{"short_id":"gnd8bc","short_id_url":"https://lobste.rs/s/gnd8bc","created_at":"2020-09-21T11:56:04.000-05:00","title":"scalar: A small chat protocol, inspired by Gemini","url":"https://sr.ht/~icefox/scalar/","score":18,"flags":0,"comment_count":4,"description":"","comments_url":"https://lobste.rs/s/gnd8bc/scalar_small_chat_protocol_inspired_by","submitter_user":{"username":"icefox","created_at":"2018-08-26T20:59:16.000-05:00","is_admin":false,"about":"","is_moderator":false,"karma":2818,"avatar_url":"/avatars/icefox-100.png","invited_by_user":"shanemhansen"},"tags":["networking","release","show"]},{"short_id":"bssphv","short_id_url":"https://lobste.rs/s/bssphv","created_at":"2020-09-21T14:21:46.000-05:00","title":"My Least Favorite Rust Type","url":"https://ridiculousfish.com/blog/posts/least-favorite-rust-type.html","score":23,"flags":0,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/bssphv/my_least_favorite_rust_type","submitter_user":{"username":"liftM","created_at":"2017-11-18T04:29:06.000-06:00","is_admin":false,"about":"I work on programming languages and build systems.","is_moderator":false,"karma":161,"avatar_url":"/avatars/liftM-100.png","invited_by_user":"alok","github_username":"liftM","twitter_username":"liftm2"},"tags":["rust"]},{"short_id":"d8wxhi","short_id_url":"https://lobste.rs/s/d8wxhi","created_at":"2020-09-20T20:34:16.000-05:00","title":"On the use of a life","url":"http://www.daemonology.net/blog/2020-09-20-On-the-use-of-a-life.html","score":103,"flags":5,"comment_count":12,"description":"","comments_url":"https://lobste.rs/s/d8wxhi/on_use_life","submitter_user":{"username":"amontalenti","created_at":"2014-02-11T09:12:37.000-06:00","is_admin":false,"about":"Founder of [Parse.ly](http://parse.ly). Python, Clojure, JavaScript, \u0026 C. UNIX lover. Web hacker. Blogging at [amontalenti.com](https://amontalenti.com), tweeting at [@amontalenti](http://twitter.com/amontalenti).","is_moderator":false,"karma":930,"avatar_url":"/avatars/amontalenti-100.png","invited_by_user":"conroy"},"tags":["crypto","freebsd","person"]},{"short_id":"gxgoel","short_id_url":"https://lobste.rs/s/gxgoel","created_at":"2020-09-21T13:22:08.000-05:00","title":"Plan 9 rides again; WSL file access","url":"https://nelsonslog.wordpress.com/2019/02/16/plan-9-rides-again-wsl-file-access/","score":13,"flags":0,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/gxgoel/plan_9_rides_again_wsl_file_access","submitter_user":{"username":"awreece","created_at":"2016-01-15T00:04:45.000-06:00","is_admin":false,"about":"My hobbies are systems performance, computer security, and weird bugs. http://codearcana.com/","is_moderator":false,"karma":286,"avatar_url":"/avatars/awreece-100.png","invited_by_user":"peter"},"tags":["linux","osdev","windows"]},{"short_id":"hvr16d","short_id_url":"https://lobste.rs/s/hvr16d","created_at":"2020-09-21T05:17:04.000-05:00","title":"What are you doing this week?","url":"","score":16,"flags":0,"comment_count":20,"description":"\u003cp\u003eWhat are you doing this week? Feel free to share!\u003c/p\u003e\n\u003cp\u003eKeep in mind it’s OK to do nothing at all, too.\u003c/p\u003e\n","comments_url":"https://lobste.rs/s/hvr16d/what_are_you_doing_this_week","submitter_user":{"username":"caius","created_at":"2014-05-13T06:58:30.000-05:00","is_admin":false,"about":"Compulsive Geek, Ale Connoisseur, Head of Engineering at [SafeguardingMonitor](https://safeguardingmonitor.co.uk), Occasionally Responsible Adult.","is_moderator":false,"karma":5077,"avatar_url":"/avatars/caius-100.png","invited_by_user":"lauris","github_username":"caius","twitter_username":"Caius"},"tags":["ask","programming"]},{"short_id":"jgcvev","short_id_url":"https://lobste.rs/s/jgcvev","created_at":"2020-09-20T15:25:25.000-05:00","title":"Why Not Rust?","url":"https://matklad.github.io//2020/09/20/why-not-rust.html","score":60,"flags":0,"comment_count":25,"description":"","comments_url":"https://lobste.rs/s/jgcvev/why_not_rust","submitter_user":{"username":"notriddle","created_at":"2018-08-30T09:08:52.000-05:00","is_admin":false,"about":"https://notriddle.com/","is_moderator":false,"karma":2864,"avatar_url":"/avatars/notriddle-100.png","invited_by_user":"zimbatm","github_username":"notriddle"},"tags":["rust"]},{"short_id":"xhrskw","short_id_url":"https://lobste.rs/s/xhrskw","created_at":"2020-09-21T05:42:07.000-05:00","title":"Is it possible to hide all \"What are you doing this XXX\" threads?","url":"","score":33,"flags":0,"comment_count":21,"description":"\u003cp\u003eI’m annoyed by all these “What are you doing this XXX” threads.\nIs it possible to automatically hide them?\u003c/p\u003e\n\u003cp\u003eThey are usually tagged with “ask” and “programming”.\nSo hiding all “ask” and “programming” threads in my settings\nprobably would do the trick,\nbut then I’d also miss the interesting questions…\u003c/p\u003e\n","comments_url":"https://lobste.rs/s/xhrskw/is_it_possible_hide_all_what_are_you_doing","submitter_user":{"username":"hwj","created_at":"2019-05-30T00:12:36.000-05:00","is_admin":false,"about":"computer science student","is_moderator":false,"karma":763,"avatar_url":"/avatars/hwj-100.png","invited_by_user":"nickpsecurity"},"tags":["meta"]},{"short_id":"xuvwrb","short_id_url":"https://lobste.rs/s/xuvwrb","created_at":"2020-09-21T10:22:37.000-05:00","title":"The unrealized potential of federation","url":"https://drewdevault.com/2020/09/20/The-potential-of-federation.html","score":7,"flags":1,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/xuvwrb/unrealized_potential_federation","submitter_user":{"username":"zge","created_at":"2017-11-19T11:49:35.000-06:00","is_admin":false,"about":"CS student and hobby developer.","is_moderator":false,"karma":9038,"avatar_url":"/avatars/zge-100.png","invited_by_user":"josuah","github_username":"phikal"},"tags":["networking"]},{"short_id":"9t1ves","short_id_url":"https://lobste.rs/s/9t1ves","created_at":"2020-09-21T06:53:28.000-05:00","title":"Creating a Home IPv6 Network","url":"https://blog.hansenpartnership.com/creating-a-home-ipv6-network/","score":14,"flags":0,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/9t1ves/creating_home_ipv6_network","submitter_user":{"username":"freddyb","created_at":"2017-02-02T09:12:16.000-06:00","is_admin":false,"about":"Security. Mostly in Browsers, but not exclusively.","is_moderator":false,"karma":3873,"avatar_url":"/avatars/freddyb-100.png","invited_by_user":"stas","keybase_signatures":[{"kb_username":"freddyb","sig_hash":"550e2f5b27d4b5d558c02dfb2b23a628e90635183c93f993eeea4e16b20c51150f"}]},"tags":["linux","networking"]},{"short_id":"8m7ydc","short_id_url":"https://lobste.rs/s/8m7ydc","created_at":"2020-09-21T11:18:06.000-05:00","title":"dstask: Single binary terminal-based TODO manager with git-based sync + markdown notes per task","url":"https://github.com/naggie/dstask","score":5,"flags":0,"comment_count":4,"description":"\u003cp\u003eShowcasing on behalf of the author\u003c/p\u003e\n","comments_url":"https://lobste.rs/s/8m7ydc/dstask_single_binary_terminal_based_todo","submitter_user":{"username":"JordiGH","created_at":"2014-11-24T16:21:05.000-06:00","is_admin":false,"about":"Jordi Gutiérrez Hermoso. \r\n\r\nGNU Octave dev, Mercurial enthusiast.\r\n\r\nCoder, mathematician, hacker-errant.\r\n\r\nYou may contact me at jordigh@octave.org","is_moderator":false,"karma":6718,"avatar_url":"/avatars/JordiGH-100.png","invited_by_user":"technomancy"},"tags":["go","show"]},{"short_id":"lzaycw","short_id_url":"https://lobste.rs/s/lzaycw","created_at":"2020-09-21T13:01:05.000-05:00","title":"Maybe don’t write off Scala just yet","url":"https://levelup.gitconnected.com/maybe-dont-write-off-scala-just-yet-f0c128a570f0","score":5,"flags":0,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/lzaycw/maybe_don_t_write_off_scala_just_yet","submitter_user":{"username":"asteroid","created_at":"2020-01-29T13:58:14.000-06:00","is_admin":false,"about":"Writer. Editor. Computer geek. Chocoholic. Baseball fan. Not always in that order.","is_moderator":false,"karma":333,"avatar_url":"/avatars/asteroid-100.png","invited_by_user":"petdance","twitter_username":"estherschindler"},"tags":["scala"]},{"short_id":"uqiz1y","short_id_url":"https://lobste.rs/s/uqiz1y","created_at":"2020-09-21T16:37:37.000-05:00","title":"Croquet Project Demo (2003)","url":"https://www.youtube.com/watch?v=cXGLOiZUZ2U","score":3,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/uqiz1y/croquet_project_demo_2003","submitter_user":{"username":"sevan","created_at":"2013-06-02T17:42:02.000-05:00","is_admin":false,"about":"","is_moderator":false,"karma":6732,"avatar_url":"/avatars/sevan-100.png","invited_by_user":"jturner","github_username":"sevan"},"tags":["graphics","video"]},{"short_id":"9tdnvp","short_id_url":"https://lobste.rs/s/9tdnvp","created_at":"2020-09-20T12:40:46.000-05:00","title":"uMatrix development has ended","url":"https://www.ghacks.net/2020/09/20/umatrix-development-has-ended/","score":38,"flags":1,"comment_count":13,"description":"","comments_url":"https://lobste.rs/s/9tdnvp/umatrix_development_has_ended","submitter_user":{"username":"skrzyp","created_at":"2016-05-21T16:41:57.000-05:00","is_admin":false,"about":"````\r\n````","is_moderator":false,"karma":867,"avatar_url":"/avatars/skrzyp-100.png","invited_by_user":"mulander"},"tags":["browsers","privacy","web"]},{"short_id":"mi4tlk","short_id_url":"https://lobste.rs/s/mi4tlk","created_at":"2020-09-21T08:28:38.000-05:00","title":"Deep Learning in Clojure with Fewer Parentheses than Keras and Python","url":"https://dragan.rocks/articles/20/Deep-Diamond-Deep-Learning-in-Clojure-Fewer-Parentheses-Python-Keras","score":4,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/mi4tlk/deep_learning_clojure_with_fewer","submitter_user":{"username":"dragandj","created_at":"2016-05-12T13:12:22.000-05:00","is_admin":false,"about":"","is_moderator":false,"karma":154,"avatar_url":"/avatars/dragandj-100.png","invited_by_user":"mindcrime"},"tags":["ai","clojure","java","python"]},{"short_id":"ztsooj","short_id_url":"https://lobste.rs/s/ztsooj","created_at":"2020-09-21T06:39:34.000-05:00","title":"14nm and 7nm are NOT what you think it is","url":"https://www.youtube.com/watch?v=1kQUXpZpLXI","score":7,"flags":0,"comment_count":2,"description":"","comments_url":"https://lobste.rs/s/ztsooj/14nm_7nm_are_not_what_you_think_it_is","submitter_user":{"username":"asymptotically","created_at":"2019-08-11T09:22:13.000-05:00","is_admin":false,"about":"","is_moderator":false,"karma":558,"avatar_url":"/avatars/asymptotically-100.png","invited_by_user":"gerikson"},"tags":["hardware","video"]},{"short_id":"wgfc92","short_id_url":"https://lobste.rs/s/wgfc92","created_at":"2020-09-21T13:43:50.000-05:00","title":"EDN parser and generator for TS/JS working with plain data and stream support","url":"https://github.com/jorinvo/edn-data","score":1,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/wgfc92/edn_parser_generator_for_ts_js_working","submitter_user":{"username":"jorin","created_at":"2020-07-06T11:34:49.000-05:00","is_admin":false,"about":"✨ decentralize all the things ✨ #clojure is just data 🖤 \r\n🌱 plant trees 🌳\r\n→ experimenting with feedback systems 🤖\r\nFind me at https://mas.to/@jorin","is_moderator":false,"karma":52,"avatar_url":"/avatars/jorin-100.png","invited_by_user":"jussi","github_username":"jorinvo","twitter_username":"jorinvo"},"tags":["clojure","javascript","nodejs","release"]},{"short_id":"bhttyk","short_id_url":"https://lobste.rs/s/bhttyk","created_at":"2020-09-20T11:28:13.000-05:00","title":"organice (Org mode for mobile devices and the browser) renders clickable links automatically","url":"https://200ok.ch/posts/2020-09-20_organice_renders_clickable_links_automatically.html","score":20,"flags":0,"comment_count":7,"description":"","comments_url":"https://lobste.rs/s/bhttyk/organice_org_mode_for_mobile_devices","submitter_user":{"username":"munen","created_at":"2019-09-23T02:27:28.000-05:00","is_admin":false,"about":"CEO 200ok.ch, Lecturer at ZHAW. Ordained Zen Buddhist monk and caretaker of the Lambda Zen Temple (http://zen-temple.net).","is_moderator":false,"karma":296,"avatar_url":"/avatars/munen-100.png","invited_by_user":"bandali","github_username":"munen"},"tags":["emacs"]},{"short_id":"lfslfx","short_id_url":"https://lobste.rs/s/lfslfx","created_at":"2020-09-21T04:28:22.000-05:00","title":"Demystifying AWS VPC","url":"https://scorpil.com/post/aws-vpc/","score":8,"flags":0,"comment_count":0,"description":"\u003cp\u003eThis is posted on behalf of \u003ca href=\"https://lobste.rs/u/scorpil\" rel=\"ugc\"\u003e@scorpil\u003c/a\u003e, who can’t post their own stuff yet. I found it relevant to this community (but as I’m their inviter I might be biased)\u003c/p\u003e\n","comments_url":"https://lobste.rs/s/lfslfx/demystifying_aws_vpc","submitter_user":{"username":"gerikson","created_at":"2017-01-13T03:16:10.000-06:00","is_admin":false,"about":"Swede, father and husband. \r\n\r\nhttp://gerikson.com/blog/\r\n\r\n### Invitation policy\r\n\r\nNote that this is my personal policy. If I don't extend an invite, it doesn't mean no-one else will. Be polite, be open, and be patient.\r\n\r\nFirst step, please reach out to the community in [chat](https://lobste.rs/chat) and let us know why you want to be a part of the community. \r\n\r\nI will typically request some form of proof that your username is associated with a public profile on a social network (and yes, I count GitHub as a social network). If you do not wish to share such information in the general chat, I will not consider extending an invite. \r\n\r\nI will probably not extend an invite if your online behavior exhibits sexism, misogyny, racism or homophobia. \r\n\r\nI will politely ignore unsolicitated requests for invites via Twitter DM or email.","is_moderator":false,"karma":7097,"avatar_url":"/avatars/gerikson-100.png","invited_by_user":"varjag","github_username":"gustafe","twitter_username":"gerikson"},"tags":["distributed","networking"]},{"short_id":"ote3wb","short_id_url":"https://lobste.rs/s/ote3wb","created_at":"2020-09-21T03:17:38.000-05:00","title":"D Tetris running on Webassembly","url":"http://dpldocs.info/this-week-in-d/Blog.Posted_2020_08_10.html","score":8,"flags":0,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/ote3wb/d_tetris_running_on_webassembly","submitter_user":{"username":"speps","created_at":"2017-09-06T02:15:19.000-05:00","is_admin":false,"about":"Senior Software Engineer at Rare Ltd (Microsoft Studios UK). All opinions are my own and do not reflect my employer's.","is_moderator":false,"karma":233,"avatar_url":"/avatars/speps-100.png","invited_by_user":"mikejsavage"},"tags":["d","wasm"]},{"short_id":"vkm0ad","short_id_url":"https://lobste.rs/s/vkm0ad","created_at":"2020-09-21T07:59:49.000-05:00","title":"k2k20 hackathon report: Klemens Nanni on network land decluttering","url":"http://undeadly.org/cgi?action=article;sid=20200921110059","score":5,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/vkm0ad/k2k20_hackathon_report_klemens_nanni_on","submitter_user":{"username":"calvin","created_at":"2014-07-01T06:47:13.000-05:00","is_admin":false,"about":"Soon we will all have special names... names designed to make the cathode ray tube resonate.","is_moderator":false,"karma":66870,"avatar_url":"/avatars/calvin-100.png","invited_by_user":"nbyouri","github_username":"NattyNarwhal"},"tags":["openbsd"]},{"short_id":"liy030","short_id_url":"https://lobste.rs/s/liy030","created_at":"2020-09-21T14:21:28.000-05:00","title":"Local memoized recursive functions","url":"https://quanttype.net/posts/2020-09-20-local-memoized-recursive-functions.html","score":2,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/liy030/local_memoized_recursive_functions","submitter_user":{"username":"jussi","created_at":"2016-06-07T11:38:09.000-05:00","is_admin":false,"about":"Programmer at Metosin\r\n\r\nClojure/Python/Javascript/Rust/Ruby/DuckDuckGo.\r\n\r\n","is_moderator":false,"karma":407,"avatar_url":"/avatars/jussi-100.png","invited_by_user":"flyingfisch","github_username":"jrasanen","twitter_username":"jussiras"},"tags":["clojure","programming"]},{"short_id":"lcb5us","short_id_url":"https://lobste.rs/s/lcb5us","created_at":"2020-09-21T08:56:55.000-05:00","title":"Analyzing Python Code with Python","url":"https://rotemtam.com/2020/08/13/python-ast/","score":4,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/lcb5us/analyzing_python_code_with_python","submitter_user":{"username":"learnbyexample","created_at":"2020-06-15T09:51:10.000-05:00","is_admin":false,"about":"Sundeep Agarwal is a freelance trainer, [author](https://learnbyexample.github.io/books/) and mentor. You can find his works, primarily focused on Linux command line, text processing, scripting languages and curated lists, at [https://github.com/learnbyexample](https://github.com/learnbyexample).","is_moderator":false,"karma":347,"avatar_url":"/avatars/learnbyexample-100.png","invited_by_user":"ngoldbaum"},"tags":["python","testing"]},{"short_id":"rmlqmn","short_id_url":"https://lobste.rs/s/rmlqmn","created_at":"2020-09-21T13:19:01.000-05:00","title":"Data art posters about music (streaming) data for Sony Music","url":"https://www.visualcinnamon.com/2020/06/sony-music-data-art","score":2,"flags":1,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/rmlqmn/data_art_posters_about_music_streaming","submitter_user":{"username":"danburzo","created_at":"2018-10-02T10:53:45.000-05:00","is_admin":false,"about":"Building things for the web. Learning in public. Co-founder of [Moqups](https://moqups.com). Find me [on Mastodon](https://mastodon.social/@danburzo).","is_moderator":false,"karma":1159,"avatar_url":"/avatars/danburzo-100.png","invited_by_user":"migurski","github_username":"danburzo","twitter_username":"danburzo"},"tags":["design","visualization"]},{"short_id":"zqyydb","short_id_url":"https://lobste.rs/s/zqyydb","created_at":"2020-09-21T07:11:14.000-05:00","title":"k2k20 hackathon report: Bob Beck on LibreSSL progress","url":"https://undeadly.org/cgi?action=article;sid=20200921105847","score":4,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on","submitter_user":{"username":"Vigdis","created_at":"2017-02-27T21:08:14.000-06:00","is_admin":false,"about":"Alleycat for the fun, sys/net admin for a living and OpenBSD contributions for the pleasure. (Not so) French dude in Montreal\r\n\r\nhttps://chown.me","is_moderator":false,"karma":76,"avatar_url":"/avatars/Vigdis-100.png","invited_by_user":"sevan"},"tags":["openbsd"]}] \ No newline at end of file diff --git a/api/src/test/resources/post_details_d9ucpe.json b/api/src/test/resources/post_details_d9ucpe.json new file mode 100644 index 00000000..e4722a80 --- /dev/null +++ b/api/src/test/resources/post_details_d9ucpe.json @@ -0,0 +1 @@ +{"short_id":"d9ucpe","short_id_url":"https://lobste.rs/s/d9ucpe","created_at":"2021-04-03T16:16:02.000-05:00","title":"What problems do people solve with strace?","url":"https://jvns.ca/blog/2021/04/03/what-problems-do-people-solve-with-strace/","score":18,"flags":0,"comment_count":7,"description":"","comments_url":"https://lobste.rs/s/d9ucpe/what_problems_do_people_solve_with_strace","submitter_user":{"username":"technetium","created_at":"2020-02-06T19:52:39.000-06:00","is_admin":false,"about":"A sentient lump of a certain high-density material","is_moderator":false,"karma":1811,"avatar_url":"/avatars/technetium-100.png","invited_by_user":"ngoldbaum"},"tags":["programming"],"comments":[{"short_id":"42bzeq","short_id_url":"https://lobste.rs/c/42bzeq","created_at":"2021-04-03T17:43:55.000-05:00","updated_at":"2021-04-03T17:43:55.000-05:00","is_deleted":false,"is_moderated":false,"score":2,"flags":0,"comment":"\u003cp\u003eI’ve always felt like my usage of strace was very contrived, but this makes me feel much better, thank you!\u003c/p\u003e\n","url":"https://lobste.rs/s/d9ucpe/what_problems_do_people_solve_with_strace#c_42bzeq","indent_level":1,"commenting_user":{"username":"garymoon","created_at":"2019-01-08T12:00:44.000-06:00","is_admin":false,"about":"","is_moderator":false,"karma":8,"avatar_url":"/avatars/garymoon-100.png","invited_by_user":"jonahx"}},{"short_id":"kejbct","short_id_url":"https://lobste.rs/c/kejbct","created_at":"2021-04-03T17:31:39.000-05:00","updated_at":"2021-04-03T17:31:39.000-05:00","is_deleted":false,"is_moderated":false,"score":2,"flags":0,"comment":"\u003cp\u003e\u003ccode\u003estrace\u003c/code\u003e is an invaluable tool for reverse engineering and learning. Recently I wanted to know exactly what happened when I played with a Rust \u003ccode\u003eio_uring\u003c/code\u003e wrapper. I could see how syscalls were made to learn more about it. I did the same when I wanted to quickly know how \u003ccode\u003etail -f\u003c/code\u003e was implemented under the hood without reading any C source code. (Btw, it uses \u003ccode\u003einotify\u003c/code\u003e + \u003ccode\u003eepoll\u003c/code\u003e if I remember correctly.)\u003c/p\u003e\n","url":"https://lobste.rs/s/d9ucpe/what_problems_do_people_solve_with_strace#c_kejbct","indent_level":1,"commenting_user":{"username":"enz","created_at":"2019-02-24T08:01:30.000-06:00","is_admin":false,"about":"Independent software engineer living in France. I like Python, ML languages, Linux and I am learning Rust. I run a small software company called [Piqotera](https://www.piqotera.com/en/) in France.\r\n\r\nMy blog: https://www.ecalamia.com/blog/","is_moderator":false,"karma":853,"avatar_url":"/avatars/enz-100.png","invited_by_user":"gerikson","github_username":"enzzc","twitter_username":"CalamiaEnzo","keybase_signatures":[{"kb_username":"enz","sig_hash":"dee15db718701675cf1e5bdd5c59b5fe54f9d6a9718ecfcc7be6bd083b2986920f"}]}},{"short_id":"vvs5qn","short_id_url":"https://lobste.rs/c/vvs5qn","created_at":"2021-04-03T20:38:09.000-05:00","updated_at":"2021-04-03T20:38:09.000-05:00","is_deleted":false,"is_moderated":false,"score":2,"flags":0,"comment":"\u003cp\u003eltrace is also quite useful.\u003c/p\u003e\n","url":"https://lobste.rs/s/d9ucpe/what_problems_do_people_solve_with_strace#c_vvs5qn","indent_level":1,"commenting_user":{"username":"jxy","created_at":"2017-08-03T12:24:47.000-05:00","is_admin":false,"about":" \\int d\\eta_i =0 \r\n\r\n \\int d\\eta_i \\eta_i = 1 \r\n\r\nRubber Duck method of debugging, the \r\n\r\n1. Beg, borrow, steal, buy, fabricate or otherwise obtain a rubber duck \r\n (bathtub variety) \r\n2. Place rubber duck on desk and inform it you are just going to go over \r\n some code with it, if that's all right. \r\n3. Explain to the duck what you code is supposed to do, and then go into \r\n detail and explain things line by line \r\n4. At some point you will tell the duck what you are doing next and then \r\n realise that that is not in fact what you are actually doing. The duck \r\n will sit there serenely, happy in the knowledge that it has helped you \r\n on your way. ","is_moderator":false,"karma":455,"avatar_url":"/avatars/jxy-100.png","invited_by_user":"friendlysock"}},{"short_id":"6gmzsf","short_id_url":"https://lobste.rs/c/6gmzsf","created_at":"2021-04-03T23:57:47.000-05:00","updated_at":"2021-04-03T23:57:47.000-05:00","is_deleted":false,"is_moderated":false,"score":1,"flags":0,"comment":"\u003cp\u003eIs there a working strace equivalent for Windows? It’s the tool I always miss when I have to debug anything there.\u003c/p\u003e\n","url":"https://lobste.rs/s/d9ucpe/what_problems_do_people_solve_with_strace#c_6gmzsf","indent_level":1,"commenting_user":{"username":"dmbaturin","created_at":"2018-08-08T16:44:02.000-05:00","is_admin":false,"about":"","is_moderator":false,"karma":2258,"avatar_url":"/avatars/dmbaturin-100.png","invited_by_user":"erkin","github_username":"dmbaturin","twitter_username":"dmbaturin"}},{"short_id":"flr1hj","short_id_url":"https://lobste.rs/c/flr1hj","created_at":"2021-04-04T01:39:39.000-05:00","updated_at":"2021-04-04T01:39:39.000-05:00","is_deleted":false,"is_moderated":false,"score":1,"flags":0,"comment":"\u003cp\u003eProcmon?\u003c/p\u003e\n","url":"https://lobste.rs/s/d9ucpe/what_problems_do_people_solve_with_strace#c_flr1hj","indent_level":2,"commenting_user":{"username":"kodfodrasz","created_at":"2018-03-16T03:12:36.000-05:00","is_admin":false,"about":"I'm a software developer living in Hungary.\r\n","is_moderator":false,"karma":943,"avatar_url":"/avatars/kodfodrasz-100.png","invited_by_user":"gerikson"}},{"short_id":"oeggxb","short_id_url":"https://lobste.rs/c/oeggxb","created_at":"2021-04-03T21:01:58.000-05:00","updated_at":"2021-04-03T21:01:58.000-05:00","is_deleted":false,"is_moderated":false,"score":1,"flags":0,"comment":"\u003cp\u003eI saw the headline and started thinking about it some before reading the article… only to discover that I’ve used strace on \u003cem\u003eall\u003c/em\u003e the problems they’ve listed. Fantastic list!\u003c/p\u003e\n","url":"https://lobste.rs/s/d9ucpe/what_problems_do_people_solve_with_strace#c_oeggxb","indent_level":1,"commenting_user":{"username":"tonyarkles","created_at":"2021-02-18T10:08:33.000-06:00","is_admin":false,"about":"Lead \"flying system designer\" at a drone startup. Love solving hard tech problems.","is_moderator":false,"karma":28,"avatar_url":"/avatars/tonyarkles-100.png","invited_by_user":"hoistbypetard"}},{"short_id":"pvti5i","short_id_url":"https://lobste.rs/c/pvti5i","created_at":"2021-04-03T19:55:48.000-05:00","updated_at":"2021-04-03T19:55:48.000-05:00","is_deleted":false,"is_moderated":false,"score":1,"flags":0,"comment":"\u003cp\u003eA recent “why is this process hanging?” I needed to turn to strace for: a daemon was trying to send mail out, but it had either connected to a STARTTLS-capable port expecting TLS, or vice versa, forget which. It hung until timeout because the handshake wasn’t working in any way, and of course it couldn’t email me the error report like it was meant to. At this stage I didn’t even know it was a mail issue. Seeing the process writing one thing to the socket and receiving another (i.e. plain SMTP one way and TLS handshake the other) made it clear as day where I’d misconfigured it.\u003c/p\u003e\n","url":"https://lobste.rs/s/d9ucpe/what_problems_do_people_solve_with_strace#c_pvti5i","indent_level":1,"commenting_user":{"username":"kameliya","created_at":"2014-01-12T22:27:37.000-06:00","is_admin":false,"about":"","is_moderator":false,"karma":2212,"avatar_url":"/avatars/kameliya-100.png","invited_by_user":"ralish"}}]} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 63991a6b..bdfbdd00 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,8 @@ buildscript { repositories { + google() + mavenCentral() gradlePluginPortal() - jcenter() - google() - mavenCentral() } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10") @@ -18,21 +17,19 @@ version = "1.0" allprojects { repositories { - jcenter() mavenCentral() maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } + google() } apply(plugin = "com.diffplug.spotless") configure { kotlin { target("**/*.kt") targetExclude("**/build/**") - ktlint().userData(mapOf("indent_size" to "2", "continuation_indent_size" to "2")) ktfmt().googleStyle() } kotlinGradle { target("*.gradle.kts") - ktlint().userData(mapOf("indent_size" to "2", "continuation_indent_size" to "2")) ktfmt().googleStyle() } format("xml") { diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index d54f3289..1f0b2563 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -15,7 +15,7 @@ kotlin { sourceSets { val jvmMain by getting { dependencies { - implementation(project(":common")) + implementation(projects.common) implementation(compose.desktop.currentOs) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..07ab6c14 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,21 @@ +[versions] +coroutines = "1.5.0" +kotlin = "1.5.10" +moshix = "0.11.2" +retrofit = "2.9.0" + +[libraries] + +kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlin-coroutines-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "coroutines" } + +thirdparty-moshi-lib = "com.squareup.moshi:moshi:1.12.0" +thirdparty-moshix-ksp = { module = "dev.zacsweers.moshix:moshi-ksp", version.ref = "moshix" } +thirdparty-moshix-metadatareflect = { module = "dev.zacsweers.moshix:moshi-metadata-reflect", version.ref = "moshix" } + +thirdparty-retrofit-lib = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +thirdparty-retrofit-moshiConverter = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } + +testing-kotlintest-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +testing-mockWebServer = "com.squareup.okhttp3:mockwebserver3-junit4:5.0.0-alpha.2" diff --git a/settings.gradle.kts b/settings.gradle.kts index 8af3dfc4..f916bc25 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,17 +1,22 @@ pluginManagement { repositories { google() - jcenter() - gradlePluginPortal() mavenCentral() maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } + gradlePluginPortal() } } rootProject.name = "Claw" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +enableFeaturePreview("VERSION_CATALOGS") + include(":android") -include(":desktop") +include(":api") include(":common") + +include(":desktop")