Skip to content

Instantly share code, notes, and snippets.

@tateisu

tateisu/Main.kt Secret

Last active January 27, 2021 00:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tateisu/801b91b7b9b75374d289eeaa9cc318ca to your computer and use it in GitHub Desktop.
Save tateisu/801b91b7b9b75374d289eeaa9cc318ca to your computer and use it in GitHub Desktop.
Hexoのdb.jsonを読んでblog記事のURLとタイトルをLemmyに投稿するコード
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.default
import kotlinx.cli.required
import kotlinx.coroutines.runBlocking
import java.io.File
import java.io.FileInputStream
import java.text.SimpleDateFormat
val parser = ArgParser("HexoToLemmy")
val verbose by parser.option(ArgType.Boolean, shortName = "v", description = "verbose logging").default(false)
val hexoDbFile by parser.option(ArgType.String, shortName = "i", description = "file path of hexo's db.json").required()
val hexoUrlPrefix by parser.option(ArgType.String, shortName = "p", description = "url prefix of hexo blog").required()
val lemmyUser by parser.option(ArgType.String, fullName = "lemmyUser", description = "lemmy account login user").required()
val lemmyPassword by parser.option(ArgType.String, shortName = "lemmyPassword", description = "lemmy account login password").required()
private const val lemmyUrlPrefix = "https://lemmy.juggler.jp/api/v2"
private const val lemmyCommunityName = "bottom_audio"
//////////////////////////////////////////////////////////////////////
private var lemmyAuth = ""
private var lemmyCommunityId = -1L
//
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
private val Zto0000 = """Z\z""".toRegex()
fun String.parseTime() = dateFormat.parse(this.replace(Zto0000, "+0000")).time
fun JsonObject.encodeQuery() =
entries.joinToString("&") { it.key.encodePercent() + "=" + it.value.toString().encodePercent() }
/*
GETの場合rate limit は 503 になる
POSTの場合 5xx { "error" : "..." } になる
*/
suspend fun HttpClient.lemmyApi(
method: HttpMethod,
endpoint: String,
form: JsonObject? = null,
): String {
if (method == HttpMethod.Get) {
val url = "$lemmyUrlPrefix$endpoint" + if (form == null) {
""
} else {
if (lemmyAuth.isNotEmpty()) form["auth"] = lemmyAuth
"?${form.encodeQuery()}".also { form.remove("auth") }
}
get<HttpResponse>(url) {
}.let { res ->
when (res.status) {
HttpStatusCode.OK -> {
return res.receive<ByteArray>().decodeUtf8()
}
else -> error("lemmyApi failed. ${res.status} $method $url")
}
}
} else {
val url = "$lemmyUrlPrefix$endpoint"
request<HttpResponse>(url) {
this.method = method
if (form != null) {
header("Content-Type", "application/json")
if (lemmyAuth.isNotEmpty()) form["auth"] = lemmyAuth
this.body = form.toString().encodeUtf8()
}
}.let { res ->
form?.remove("auth")
when (res.status) {
HttpStatusCode.OK -> {
return res.receive<ByteArray>().decodeUtf8()
}
else -> {
var content = try {
res.receive<ByteArray>().decodeUtf8()
} catch (_: Throwable) {
null
}
try{
content?.decodeJsonObject()?.string("error")?.notEmpty()?.let{ content = it}
}catch (_: Throwable) {
}
error("lemmyApi failed. ${res.status} $method $url $content")
}
}
}
}
}
suspend fun HttpClient.postToLemmy(blogUrl: String, blogTitle: String) {
if (lemmyAuth.isEmpty()) {
lemmyAuth = lemmyApi(HttpMethod.Post,
"/user/login",
jsonObject("username_or_email" to lemmyUser, "password" to lemmyPassword))
.decodeJsonObject()
.string("jwt")
?: error("missing jwt in LoginResponse")
println("lemmy login succeeded.")
}
// 投稿先のコミュニティーIDを知りたい
if (lemmyCommunityId < 0L) {
lemmyCommunityId = lemmyApi(HttpMethod.Get, "/community", jsonObject("name" to lemmyCommunityName))
.decodeJsonObject().let { root ->
root.jsonObject("community_view")?.jsonObject("community")?.long("id")
?: error("can't get community id. $root")
}
println("community: $lemmyCommunityId $lemmyCommunityName")
}
// WebUIではWebSocketで処理されている
// {"op":"Search","data":{"q":"https://www.victor.jp/headphones/in-ear/ha-fw10000/","type_":"Url","sort":"TopAll","page":1,"limit":6,"auth":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MywiaXNzIjoibGVtbXkuanVnZ2xlci5qcCJ9.fZlkzOwQhZcbLsBBB2E8qwGk4HvSnDKuZB2WRqWhWLQ"}}
lemmyApi(
HttpMethod.Get,
"/search",
jsonObject(
"q" to blogUrl,
"type_" to "Url",
"sort" to "TopAll",
"page" to 1,
"limit" to 3
)
).decodeJsonObject().let { root ->
val oldPost = root.jsonArray("posts")?.objectList()
?.find { it.jsonObject("community")?.string("name") == lemmyCommunityName }
if (oldPost != null) {
println("$blogUrl already exists. ${oldPost.jsonObject("post")?.string("ap_id")}")
return
}
}
val lemmyUrl = lemmyApi(HttpMethod.Post, "/post", jsonObject(
"name" to blogTitle,
"url" to blogUrl,
"nsfw" to false,
"community_id" to lemmyCommunityId
)).decodeJsonObject().let { root ->
root.jsonObject("post_view")?.jsonObject("post")?.string("ap_id")
?: error("can't get lemmyUrl. $root")
}
println("$lemmyUrl created.")
}
data class BlogPost(
val url: String,
val title: String,
val time: Long,
)
suspend fun HttpClient.mainImpl() {
val urlPrefix = if (hexoUrlPrefix.endsWith("/")) {
hexoUrlPrefix
} else {
"$hexoUrlPrefix/"
}
val root = FileInputStream(File(hexoDbFile)).use { it.readBytes() }.toString(Charsets.UTF_8).decodeJsonObject()
val posts = root.jsonObject("models")?.jsonArray("Post")?.objectList()
?: error("can't find models.Post")
println("${posts.size} posts found.")
posts.mapNotNull { post ->
val title = post.string("title")
val url = "$urlPrefix${post.string("slug")}"
val time = post.string("date")?.parseTime() ?: error("date is null")
if (post.boolean("published") != true) {
println("$url is not published.")
null
} else {
BlogPost(url = url, title = title ?: url, time = time)
}
}
.sortedBy { it.time }
.forEach { post ->
get<HttpResponse>(post.url) {
// header(it.key, it.value)
}.let { res ->
when (res.status) {
HttpStatusCode.OK -> {
println("${post.url} ${post.title}")
postToLemmy(post.url, post.title)
}
else -> error("get failed. ${res.status} ${post.url}")
}
}
}
}
fun main(args: Array<String>) = runBlocking {
parser.parse(args)
HttpClient {
// エラーレスポンスで例外を出さない
expectSuccess = false
install(HttpTimeout) {
val t = 30000L
requestTimeoutMillis = t
connectTimeoutMillis = t
socketTimeoutMillis = t
}
}.use { client ->
client.mainImpl()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment