<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Light_Quanta&apos;s Site</title><description>No description</description><link>https://lq0.tech/</link><language>zh_CN</language><item><title>使用Kotlin开发Bukkit插件的命令处理DSL</title><link>https://lq0.tech/posts/ktplugindsl/</link><guid isPermaLink="true">https://lq0.tech/posts/ktplugindsl/</guid><description>使用Kotlin的DSL特性简化Minecraft Bukkit插件命令处理</description><pubDate>Sat, 13 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文适合有Java基础并且想了解下Kotlin的人阅读，没有Java或别的程序语言基础的不建议继续阅读（&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近在开发一个Minecraft Bukkit服务端插件，需要写一个玩家命令处理。由于Bukkit原版的命令处理非常答辩，所以基本上插件都是自己实现了一套命令处理系统。&lt;/p&gt;
&lt;p&gt;先来看看Bukkit给&lt;code&gt;JavaPlugin&lt;/code&gt;插件提供的&lt;code&gt;onCommand&lt;/code&gt;事件的签名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boolean onCommand(CommandSender sender, Command command, String label, String[] args)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到该事件仅仅是传递了命令发送者&lt;code&gt;sender&lt;/code&gt;、命令本体&lt;code&gt;command&lt;/code&gt;、标签&lt;code&gt;label&lt;/code&gt;和后续的参数&lt;code&gt;args&lt;/code&gt;，对于命令的处理则是完全没有提供对应的方法。因此，你不得不自己研究一套命令处理机制。&lt;/p&gt;
&lt;p&gt;比如&lt;a href=&quot;https://github.com/PlayPro/CoreProtect/blob/66b77ee75a24dac13f9cbf95242b504e1494dde7/src/main/java/net/coreprotect/command/CommandHandler.java#L1154&quot;&gt;CoreProtect&lt;/a&gt;就是嗯写了几十个&lt;code&gt;if ... else ...&lt;/code&gt;来处理的（&lt;/p&gt;
&lt;p&gt;正好我前段时间在写的插件&lt;a href=&quot;https://github.com/LightQuanta/InteractionSupervisor&quot;&gt;InteractionSupervisor&lt;/a&gt;是用Kotlin进行开发的，而Kotlin正好&lt;a href=&quot;https://kotlinlang.org/docs/type-safe-builders.html&quot;&gt;适合用于写DSL&lt;/a&gt;（比如gradle就可以使用build.gradle.kts进行配置），因此我就用Kotlin写了个迫真命令处理DSL&lt;/p&gt;
&lt;h2&gt;成品展示&lt;/h2&gt;
&lt;p&gt;先看看最终效果（插件里定义的完整命令&lt;a href=&quot;https://github.com/LightQuanta/InteractionSupervisor/blob/959e7e7fee642e54d184a2782431c9ec549dd644/src/main/kotlin/tech/lq0/interactionsupervisor/Main.kt#L27&quot;&gt;见此&lt;/a&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val commands = buildCommand {
    &quot;test&quot; {
        execute {
            sender.sendMessage(&quot;args=&quot; + args.joinToString(&quot;,&quot;))
        }
        tabComplete { mutableListOf(&quot;1&quot;, &quot;2&quot;, &quot;3&quot;) }
    }
    &quot;group&quot; {
        usage(&quot;group [a|b]&quot;)
        &quot;a&quot; {
            execute { sender.sendMessage(&quot;a&quot;) }
        }
        &quot;b&quot; {
            execute { sender.sendMessage(&quot;b&quot;) }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码定义了以下内容：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;test&lt;/code&gt;命令，执行后输出该命令附带的参数（如&lt;code&gt;test a b c&lt;/code&gt;会向命令发送者发送&lt;code&gt;args=a,b,c&lt;/code&gt;），同时在输入该命令时提供补全内容&lt;code&gt;1&lt;/code&gt;、&lt;code&gt;2&lt;/code&gt;、和&lt;code&gt;3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;命令组&lt;code&gt;group&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;该命令组会要求所有子命令都以此作为开头，如下文定义的命令&lt;code&gt;a&lt;/code&gt;和&lt;code&gt;b&lt;/code&gt;需要以&lt;code&gt;group a&lt;/code&gt;和&lt;code&gt;group b&lt;/code&gt;的方式调用&lt;/li&gt;
&lt;li&gt;同时命令组也会自动提供对其子命令的补全，玩家输入&lt;code&gt;group&lt;/code&gt;时，会自动提供补全内容&lt;code&gt;a&lt;/code&gt;和&lt;code&gt;b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;玩家输入错误内容时会向上查找由&lt;code&gt;usage&lt;/code&gt;定义的第一条有效的帮助内容并返回给玩家&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在&lt;code&gt;onCommand&lt;/code&gt;和&lt;code&gt;onTabComplete&lt;/code&gt;事件处调用&lt;code&gt;commands&lt;/code&gt;变量内定义的对应方法即可实现以上功能。&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;buildCommand&lt;/code&gt;即可开始构建命令处理模块，在模块中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;&quot;命令(组)名称&quot; { ... }&lt;/code&gt;即可定义一个命令&lt;/li&gt;
&lt;li&gt;在命令组中继续使用&lt;code&gt;&quot;命令名称&quot; { ... }&lt;/code&gt;即可定义子命令，同时命令组也会提供对该组命令所有子命令的补全&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;execute { ... }&lt;/code&gt;即可定义命令执行代码，其中&lt;code&gt;command&lt;/code&gt;、&lt;code&gt;sender&lt;/code&gt;、&lt;code&gt;args&lt;/code&gt;等命令参数会被一同提供至上下文中，可以直接使用&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;tabComplete { ... }&lt;/code&gt;可以定义命令的Tab补全内容&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;usage(&quot;...&quot;)&lt;/code&gt;可以定义命令组的帮助信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相比于传统的方法，使用DSL写出来的命令处理可读性更强，代码也更清晰。&lt;/p&gt;
&lt;p&gt;能实现以上功能都得归功于Kotlin的一大堆逆天语法和特性。&lt;/p&gt;
&lt;h2&gt;前置知识介绍&lt;/h2&gt;
&lt;p&gt;以上DSL的核心代码基本就这些：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Command(val name: String) {
    val subCommands = mutableListOf&amp;lt;Command&amp;gt;()
    var usage: String? = null
    var tabComplete: (() -&amp;gt; MutableList&amp;lt;String&amp;gt;) = { mutableListOf() }
    var execute: ((CommandExecuteParams) -&amp;gt; Unit)? = null

    fun tabComplete(init: () -&amp;gt; MutableList&amp;lt;String&amp;gt;) {
        tabComplete = init
    }

    fun execute(init: CommandExecuteParams.() -&amp;gt; Unit) {
        execute = init
    }

    fun usage(init: String) {
        usage = init
    }

    operator fun String.invoke(init: Command.() -&amp;gt; Unit) = Command(this).apply(init).also(subCommands::add)
}

class CommandBuilder {
    private val commands = mutableListOf&amp;lt;Command&amp;gt;()
    operator fun String.invoke(init: Command.() -&amp;gt; Unit) = Command(this).apply(init).also(commands::add)
    fun build() = CommandExecutor(commands)
}

fun buildCommand(init: CommandBuilder.() -&amp;gt; Unit) = CommandBuilder().apply(init).build()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的定义可在&lt;a href=&quot;https://github.com/LightQuanta/InteractionSupervisor/blob/959e7e7fee642e54d184a2782431c9ec549dd644/src/main/kotlin/tech/lq0/interactionsupervisor/command/CommandDSL.kt&quot;&gt;这里&lt;/a&gt;查看。&lt;/p&gt;
&lt;p&gt;该DSL涉及到大量的Kotlin特性，我们一个个来讲。&lt;/p&gt;
&lt;h3&gt;扩展函数&lt;/h3&gt;
&lt;p&gt;Kotlin可以给任意类型添加扩展函数。扩展函数我认为可以理解成一种特殊的普通函数的简写，如以下函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun String.aaa() {
    println(this)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在定义后即可在任意的&lt;code&gt;String&lt;/code&gt;变量上调用，如&lt;code&gt;&quot;test&quot;.aaa()&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;运算符重载&lt;/h3&gt;
&lt;p&gt;Kotlin支持&lt;a href=&quot;https://kotlinlang.org/docs/operator-overloading.html&quot;&gt;运算符重载&lt;/a&gt;，可以重新定义各个类型的变量对于不同的运算符的操作，使用&lt;code&gt;operator fun 运算符类型()&lt;/code&gt;即可为当前类定义运算符的重载。&lt;/p&gt;
&lt;p&gt;例如以下定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
    operator fun plus(other: Any) {
        println(&quot;A 与 $other 相加了！&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重载了类型&lt;code&gt;A&lt;/code&gt;的&lt;code&gt;plus（+）&lt;/code&gt;运算符，当你将A类型的变量与任意别的变量相加（+）时，将会在控制台输出&lt;code&gt;A 与 另一个变量 相加了！&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A() + 123
// 控制台输出：A 与 123 相加了！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运算符重载同样可以配合上述的扩展函数使用，如以下定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;operator fun String.times(count: Int) = this.repeat(count)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重载了字符串的&lt;code&gt;times（*）&lt;/code&gt;运算符和&lt;code&gt;Int&lt;/code&gt;的操作，将允许你在任意的字符串上使用&lt;code&gt;*n&lt;/code&gt;将该字符串的内容重复n遍，如&lt;code&gt;&quot;a&quot; * 5&lt;/code&gt;将得到&lt;code&gt;&quot;aaaaa&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在扩展函数里可以直接以&lt;code&gt;this&lt;/code&gt;来访问该变量本身，上文就是在用&lt;code&gt;this&lt;/code&gt;表示字符串的值。&lt;/p&gt;
&lt;p&gt;在Kotlin中还有个神秘的运算符叫&lt;code&gt;invoke&lt;/code&gt;，实现这个运算符即可将任何对象都当作一个迫真函数来使用，非常离谱。如以下定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;operator fun String.invoke() = println(this)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将允许你在任意字符串上以&lt;code&gt;&quot;xxx&quot;()&lt;/code&gt;（或&lt;code&gt;&quot;xxx&quot;.invoke()&lt;/code&gt;）的形式直接当成一个迫真函数来进行使用，效果是在控制台输出该字符串本身。&lt;/p&gt;
&lt;p&gt;如果你在一个类/接口里面定义一个其他类型的运算符重载，那么这个重载就只在这个类/接口及其子类里面可用，避免外部环境受到污染。&lt;/p&gt;
&lt;p&gt;例如我这里在&lt;code&gt;Command&lt;/code&gt;和&lt;code&gt;CommandBuilder&lt;/code&gt;里重载了&lt;code&gt;String&lt;/code&gt;的&lt;code&gt;invoke&lt;/code&gt;，使得你可以直接在一个字符串上执行代码，这样就实现了写个字符串就能定义一个命令。并且由于该重载并没有定义在全局上，这种写法只在这个DSL里面可用，不会影响到项目的其他部分。&lt;/p&gt;
&lt;p&gt;其实&lt;a href=&quot;https://kotlinlang.org/docs/type-safe-builders.html#how-it-works&quot;&gt;Kotlin的官方文档里&lt;/a&gt;也存在类似的写法，在他们的HTML示例里也是通过重载字符串的&lt;code&gt;unaryPlus（+）&lt;/code&gt;运算符，使得你可以直接使用&lt;code&gt;+&quot;xxx&quot;&lt;/code&gt;的方式直接往HTML元素里添加正文。并且同样由于该重载扩展函数只存在于这个DSL的定义中，这种重载也不会污染到外部环境，可以放心使用。
至于为什么最终成品的写法能简化成&lt;code&gt;&quot;xxx&quot; { ... }&lt;/code&gt;而不是&lt;code&gt;&quot;xxx&quot;( ... )&lt;/code&gt;，这就要看下文的lambda表达式简写了&lt;/p&gt;
&lt;h3&gt;高阶函数&lt;/h3&gt;
&lt;p&gt;和Java不同的是，Kotlin提供了强大的函数式编程的支持（Java里的lambda最多算是个SAM（单抽象方法）的语法糖）。在Kotlin里，函数是一等公民。函数不仅可以正常定义和调用，也可以像普通变量一样被存储和传递。如果一个函数的参数里有其他函数，或者说这个函数的返回值也是一个函数，那这个函数就是一个高阶函数。&lt;/p&gt;
&lt;p&gt;在Kotlin里，你可以直接声明一个函数类型的变量，写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val f : (Int, Int) -&amp;gt; Int = { a, b -&amp;gt; a + b }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码定义了变量f，该变量是一个函数，可以用&lt;code&gt;f(111, 222)&lt;/code&gt;的方式直接调用，返回的值是&lt;code&gt;333&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们可以看到这个函数变量由以下几个部分组成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;类型声明&lt;br /&gt;
&lt;code&gt;(Int, Int) -&amp;gt; Int&lt;/code&gt;&lt;br /&gt;
前面的&lt;code&gt;(Int, Int)&lt;/code&gt;表示的是这个函数所接收的参数，这里是接收两个Int。&lt;br /&gt;
后面的&lt;code&gt;-&amp;gt; Int&lt;/code&gt;表示函数的返回值类型，这里就是返回一个Int。&lt;/li&gt;
&lt;li&gt;代码块&lt;br /&gt;
&lt;code&gt;{ a, b -&amp;gt; a + b }&lt;/code&gt;&lt;br /&gt;
这里的&lt;code&gt;a, b&lt;/code&gt;就是上面说的函数所接收的参数，后面的&lt;code&gt;a + b&lt;/code&gt;就是这个函数具体执行的内容了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;类似&lt;code&gt;{ a, b -&amp;gt; a + b }&lt;/code&gt;的这种定义函数的方法有很多名称，例如匿名函数、lambda表达式、箭头函数等。&lt;/p&gt;
&lt;p&gt;匿名函数：因为用这种方式定义函数的时候，你没有说明函数的名称（正常情况下定义函数都是需要声明函数名称的），所以用这种写法就是定义了一个没有名字的匿名函数。&lt;/p&gt;
&lt;p&gt;lambda表达式：这个名字来源于数学中的λ演算，具体的我也不懂，反正在编程里lambda表达式指的就是用这种方式定义的匿名函数。&lt;/p&gt;
&lt;p&gt;箭头函数：顾名思义，在各个编程语言里，定义这种函数往往都需要使用&lt;code&gt;-&amp;gt;&lt;/code&gt;或&lt;code&gt;=&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;匿名函数的简写&lt;/h3&gt;
&lt;p&gt;只是使用以上方法还写不出这个DSL，因为按上述定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;operator fun String.invoke(init: Command.() -&amp;gt; Unit) = Command(this).apply(init).also(commands::add)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;，那要调用这个扩展函数应该是这么写的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;xxx&quot;({
    execute({
        ...
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那为什么能写成现在这样呢？这就要提到kotlin对于lambda表达式的专用简写了。&lt;/p&gt;
&lt;h4&gt;匿名函数作为函数的最后一个参数&lt;/h4&gt;
&lt;p&gt;kotlin规定，当一个匿名函数是函数的最后一个参数时，可以把它挪到小括号外面，所以上文我们可以改成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;xxx&quot;() {
    execute() {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;函数参数仅有单个匿名函数时的小括号省略&lt;/h4&gt;
&lt;p&gt;kotlin还规定了，当一个函数的参数仅有一个匿名函数时，小括号也可以省略，所以我们还可以改成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;xxx&quot; {
    execute {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就已经是成品的样子了。&lt;/p&gt;
&lt;h4&gt;对于单个参数的匿名函数的变量声明省略&lt;/h4&gt;
&lt;p&gt;虽然这里没有用到这个特性，但也单独讲一下，当lambda表达式的参数只有一个的时候，这个参数可以省略不写，kotlin会直接将&lt;code&gt;it&lt;/code&gt;作为它的名称：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val f : (Int) -&amp;gt; Int = { it * 2 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;it&lt;/code&gt;就是那个参数的名字，可以直接在lambda表达式里面使用。&lt;/p&gt;
&lt;h2&gt;最终原理讲解&lt;/h2&gt;
&lt;p&gt;讲完上述这些就可以开始讲解这个DSL究竟是如何做到的了。&lt;/p&gt;
&lt;p&gt;我们先来逐个看上文的定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;buildCommand {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有上文基础就能看懂这段在干什么了。很明显，&lt;code&gt;buildCommand&lt;/code&gt;是一个函数，接收一个匿名函数作为参数。因此，小括号可以省略，并且能把大括号写到外面，大括号里面的内容就是这个匿名函数的代码了。&lt;/p&gt;
&lt;p&gt;回头看一眼&lt;code&gt;buildCommand&lt;/code&gt;的定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun buildCommand(init: CommandBuilder.() -&amp;gt; Unit) = CommandBuilder().apply(init).build()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到这个函数接收的参数&lt;code&gt;init&lt;/code&gt;是一个针对&lt;code&gt;CommandBuilder&lt;/code&gt;的扩展函数，返回类型为Unit，即空。&lt;/p&gt;
&lt;p&gt;这个函数的作用就是创建一个&lt;code&gt;CommandBuilder&lt;/code&gt;的实例，并调用&lt;code&gt;init&lt;/code&gt;初始化它的内容，最后&lt;code&gt;build()&lt;/code&gt;得到我们需要的&lt;code&gt;CommandExecutor&lt;/code&gt;实例。&lt;/p&gt;
&lt;p&gt;这里的&lt;code&gt;apply&lt;/code&gt;表示将指定的函数作用于该对象上，这里的效果基本等同于&lt;code&gt;CommandBuilder().init()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;既然&lt;code&gt;init&lt;/code&gt;是&lt;code&gt;CommandBuilder&lt;/code&gt;的扩展函数，那这个函数的&lt;code&gt;this&lt;/code&gt;也就会被赋值为上文的&lt;code&gt;CommandBuilder&lt;/code&gt;实例，因此我们可以直接在&lt;code&gt;buildCommand { ... }&lt;/code&gt;里使用&lt;code&gt;CommandBuilder&lt;/code&gt;的成员实例&lt;code&gt;String.invoke&lt;/code&gt;（即上文使用的&lt;code&gt;&quot;xxx&quot; { ... }&lt;/code&gt;语法，看到这里就能明白这其实是在调用字符串的&lt;code&gt;invoke&lt;/code&gt;运算符操作）了。&lt;/p&gt;
&lt;p&gt;接下来我们看看核心部分，&lt;code&gt;String.invoke&lt;/code&gt;到底定义了什么：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;operator fun String.invoke(init: Command.() -&amp;gt; Unit) = Command(this).apply(init).also(commands::add)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个命令（组），可以看到传递的参数还是一个&lt;code&gt;init&lt;/code&gt;，所以依然可以使用kotlin特有的匿名函数简写。后面的代码大同小异，同样是&lt;code&gt;apply(init)&lt;/code&gt;，然后附加一个&lt;code&gt;also&lt;/code&gt;将定义的命令加到命令列表里。&lt;/p&gt;
&lt;p&gt;同样的，由于&lt;code&gt;init&lt;/code&gt;是&lt;code&gt;Command&lt;/code&gt;的扩展函数，因此也可以直接在里面使用&lt;code&gt;Command&lt;/code&gt;类的&lt;code&gt;tabComplete&lt;/code&gt;、&lt;code&gt;execute&lt;/code&gt;和&lt;code&gt;usage&lt;/code&gt;方法，以及定义子命令的同样的&lt;code&gt;String.invoke&lt;/code&gt;扩展函数。&lt;/p&gt;
&lt;p&gt;最后这几个方法就很简单了，比如&lt;code&gt;execute&lt;/code&gt;的函数签名长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun execute(init: CommandExecuteParams.() -&amp;gt; Unit) {
    execute = init
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仅仅是将&lt;code&gt;Command&lt;/code&gt;内部的&lt;code&gt;execute&lt;/code&gt;变量赋值为&lt;code&gt;init&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;同样的，由于&lt;code&gt;init&lt;/code&gt;是&lt;code&gt;CommandExecuteParams&lt;/code&gt;的扩展函数，函数内的&lt;code&gt;this&lt;/code&gt;直接指向&lt;code&gt;CommandExecuteParams&lt;/code&gt;实例，因此可以直接在里面使用&lt;code&gt;CommandExecuteParams&lt;/code&gt;内定义的命令参数而无需单独传递。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是这个迫真命令DSL的实现原理，希望本文能多少让你了解点Kotlin的逆天特性（）&lt;/p&gt;
</content:encoded></item><item><title>B站前端评论区源码逆向</title><link>https://lq0.tech/posts/bilicommentreverse/</link><guid isPermaLink="true">https://lq0.tech/posts/bilicommentreverse/</guid><description>逆向分析B站评论区前端源码，讲解如何恢复显示隐藏评论和IP属地</description><pubDate>Mon, 27 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;简介&lt;/h1&gt;
&lt;p&gt;之前看到一个UP主因为一些原因在视频底下大规模控评，我注意到在控评的评论区下，原本网页端应该每次加载返回20条评论，但实际上要少很多。我就好奇是评论本身后端没有返回还是怎么回事，就打开F12抓了下请求看看，结果发现被标记为隐藏的评论后端也在正常返回数据，只是前端隐藏了。不仅如此，我还发现评论的API还有返回评论者评论时的IP属地，只是前端没有进行渲染。于是我就打算写个脚本来把这些数据显示出来&lt;/p&gt;
&lt;p&gt;&lt;s&gt;脚本目前已经发布，详见&lt;a href=&quot;https://lq0.tech/2023/02/24/bilibilienhance/&quot;&gt;这里&lt;/a&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;以下功能已集成至&lt;a href=&quot;https://github.com/the1812/Bilibili-Evolved&quot;&gt;Bilibili Evolved&lt;/a&gt;，请搜索&lt;code&gt;IP属地显示&lt;/code&gt;、&lt;code&gt;关注时间显示&lt;/code&gt;、&lt;code&gt;相簿发布时间显示&lt;/code&gt;等组件&lt;/p&gt;
&lt;p&gt;此文就来讲解该脚本的原理&lt;/p&gt;
&lt;h1&gt;实现原理&lt;/h1&gt;
&lt;p&gt;以下是该脚本的实现原理讲解&lt;/p&gt;
&lt;h2&gt;前期准备&lt;/h2&gt;
&lt;h3&gt;API分析&lt;/h3&gt;
&lt;p&gt;B站网页端每次会请求20条评论，后端的评论API会返回评论的相关信息。评论的详细JSON格式可以参考&lt;a href=&quot;https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/comment/readme.md#%E8%AF%84%E8%AE%BA%E6%9D%A1%E7%9B%AE%E5%AF%B9%E8%B1%A1&quot;&gt;这个&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;以下是一条评论数据的示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  ...
  &quot;invisible&quot;: false,
  &quot;reply_control&quot;: {
    &quot;max_line&quot;: 6,
    &quot;time_desc&quot;: &quot;10秒前发布&quot;,
    &quot;location&quot;: &quot;IP属地：江苏&quot;
  }
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据我和网页上实际显示的评论和后端实际返回的评论比较，我发现前端会检测评论条目对象里的&lt;code&gt;invisible&lt;/code&gt;属性，若属性为空时则不会渲染评论本身。除此之外，可以发现&lt;code&gt;reply_control.location&lt;/code&gt;属性正是App端显示的IP属地，而网页端并没有对这个属性进行解析。因此，只要能实现对这两个属性进行单独处理，就能实现显示隐藏评论以及在网页端显示IP属地&lt;/p&gt;
&lt;h3&gt;技术选择&lt;/h3&gt;
&lt;p&gt;为了实现这些功能多半得直接修改B站的前端JS脚本源码，因为众所周知这年头的打包工具全都会把JS脚本压缩成IIFE运行，变量全都给整成闭包了，根本没法用JS脚本在外部进行操作，所以用TamperMonkey这种在网页上执行用户JS脚本的插件肯定没辙，得想别的办法&lt;/p&gt;
&lt;p&gt;幸好我以前凑巧就用过一个能实现这种功能的插件，&lt;a href=&quot;https://he.firefoxcn.net/&quot;&gt;Header Editor&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Header Editor除了修改Header和请求重定向等功能之外，在Firefox里也能使用自定义脚本直接修改响应体，这样就能实现修改源码的功能&lt;/p&gt;
&lt;p&gt;不过我经过研究发现Chrome等浏览器貌似不支持这么做，找了几个同类型的插件都说Chrome里无法修改响应体，估计是浏览器本身的限制
幸好这类插件的重定向请求等功能在Chrome里也可以用，可以在修改完B站前端的源码后把修改后的文件放到别的CDN上，然后把B站原先的脚本重定向到CDN那里存储的修改后的脚本，这样也能实现一样的效果&lt;/p&gt;
&lt;p&gt;最终我选择在GitHub新建一个&lt;a href=&quot;https://github.com/LightQuanta/BilibiliEnhance/&quot;&gt;仓库&lt;/a&gt;，并且使用&lt;a href=&quot;http://jsdelivr.com/&quot;&gt;jsDelivr&lt;/a&gt;作为CDN直接获取GitHub仓库里的文件，这样就能在非Firefox浏览器里也实现同样的效果&lt;/p&gt;
&lt;h2&gt;旧版评论区逆向&lt;/h2&gt;
&lt;p&gt;我是先从网页端旧版界面开始逆向的，先看看网页端的评论元素长什么样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;con &quot;&amp;gt;
  ...
  &amp;lt;div class=&quot;info&quot;&amp;gt;
    &amp;lt;span class=&quot;time-location&quot;&amp;gt;
      &amp;lt;span class=&quot;reply-time&quot;&amp;gt; xx分钟/小时前 / 评论具体时间 &amp;lt;/span&amp;gt;
      &amp;lt;span class=&quot;like &quot;&amp;gt; 点赞信息... &amp;lt;/span&amp;gt;
      ...
    &amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
  ...
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要添加自定义元素，那肯定得先找到是哪段代码创建的这个评论元素。这里的class非常明显，我们可以直接在浏览器的开发者工具里全局搜索&lt;code&gt;time-location&lt;/code&gt;、&lt;code&gt;reply-time&lt;/code&gt;等样式。以下是我在Firefox开发者工具里搜索的结果&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;reply-time-search.png&quot; alt=&quot;这不是一下就搜出来了（&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可知脚本来自&lt;a href=&quot;https://s1.hdslb.com/bfs/seed/jinkela/commentpc/comment.min.js&quot;&gt;comment.min.js&lt;/a&gt; &lt;s&gt;这个.min.js后面根本不min啊（恼）&lt;/s&gt; 。注意这里搜到了两个片段，分别是处理评论主楼和楼中楼回复的，处理的时候两个地方都得改一遍。跳转过去查看代码，我们就会发现这是在创建评论区对应的DOM元素&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;create-comment-dom.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;很明显这是在用字符串拼接的方式直接创建了HTML元素并直接插入到&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;里。但是等等，有没有注意到什么&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// item?.reply_control?.location ? `&amp;lt;span class=&quot;reply-location&quot;&amp;gt;IP属地：${item?.reply_control?.location || &apos;&apos;}&amp;lt;/span&amp;gt;` : &apos;&apos;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注释？&lt;/p&gt;
&lt;p&gt;是的，你没看错，他们已经把IP属地显示都写好了，仅仅是去掉这行注释就可以直接启用网页端IP属地显示。而且注释里写的样式&lt;code&gt;reply-location&lt;/code&gt;也是个确实存在的可用的CSS样式
这说明B站早就做好了对应的功能，只是由于某种未知的原因没有把这个功能在网页端上线
那这样的话只需要去掉这行注释就能实现IP属地显示了&lt;/p&gt;
&lt;p&gt;但我要做的可不止是这个，我要做的还有显示被隐藏评论。另外有人觉得只显示IP属地不够明显，所以我还顺便改了下IP属地的样式（&lt;/p&gt;
&lt;p&gt;简单检查下代码可以发现这一段行大致是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var con = [
  ..., 
  &apos;&amp;lt;div class=&quot;info&quot;&amp;gt;&apos;,
  this._createPlatformDom(item.content.plat),
  &quot;&amp;lt;span class=\&quot;time-location\&quot;&amp;gt;&quot;,
  &quot;&amp;lt;span class=\&quot;reply-time\&quot;&amp;gt;&quot;.concat(this._formateTime(item.ctime), &quot;&amp;lt;/span&amp;gt;&quot;), 
  // item?.reply_control?.location ? `&amp;lt;span class=&quot;reply-location&quot;&amp;gt;IP属地：${item?.reply_control?.location || &apos;&apos;}&amp;lt;/span&amp;gt;` : &apos;&apos;,
  ...
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此我们可以猜测&lt;code&gt;con&lt;/code&gt;数组就是原始HTML片段，只要我们在后面继续加就可以实现添加自己的元素&lt;/p&gt;
&lt;p&gt;同时可以注意到这其中用的参数都是&lt;code&gt;item.xxx&lt;/code&gt;，比如评论时间是取的&lt;code&gt;item.ctime&lt;/code&gt;，评论属地用的是&lt;code&gt;item?.reply_control?.location&lt;/code&gt;
因此我们可以猜测&lt;code&gt;item&lt;/code&gt;就是评论元素本身，那要检测评论是否被隐藏的话，只要取&lt;code&gt;item.invisible&lt;/code&gt;即可判断&lt;/p&gt;
&lt;p&gt;所以最终我的处理方法是把&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// item?.reply_control?.location ? `&amp;lt;span class=&quot;reply-location&quot;&amp;gt;IP属地：${item?.reply_control?.location || &apos;&apos;}&amp;lt;/span&amp;gt;` : &apos;&apos;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;替换成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;item?.reply_control?.location ? `&amp;lt;span style=&quot;margin-left:5px; color: #0CC;&quot;&amp;gt;${item?.reply_control?.location || &apos;&apos;}&amp;lt;/span&amp;gt;` : &apos;&apos;,
item?.invisible ? &apos;&amp;lt;span style=&quot;margin-left: 5px;color: red;&quot;&amp;gt;评论被隐藏&amp;lt;/span&amp;gt;&apos; : &apos;&apos;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意&lt;code&gt;IP属地：&lt;/code&gt;这几个字在返回的JSON数据里已经加上了，所以这里应该把HTML里的&lt;code&gt;IP属地：&lt;/code&gt;这几个字删掉，不然就会重复显示了
除了恢复IP属地显示之外，我加了一条判断，如果&lt;code&gt;item.invisible&lt;/code&gt;为true就添加一个&lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;用来说明评论被隐藏，否则就什么也不添加
这样就能在显示评论属地的时候也能标记出隐藏评论了&lt;/p&gt;
&lt;p&gt;但是别忘了，前文刚说了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我发现前端会检测评论条目对象里的&lt;code&gt;invisible&lt;/code&gt;属性，若属性为空时则不会渲染评论本身&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果元素直接不被创建，在这光拼接HTML也没有用，所以还得把这个判断给整掉
但有了之前的经验，要整掉这个判断就很简单了&lt;/p&gt;
&lt;p&gt;在该脚本里直接搜索&lt;code&gt;invisible&lt;/code&gt;就能发现5段相同的判断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (item.invisible) {
  return &apos;&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;明显是在评论被隐藏时不渲染评论
那想干掉这个判断就很简单了，直接把判断条件替换成&lt;code&gt;if (false)&lt;/code&gt;或者干脆删掉这段代码都行&lt;/p&gt;
&lt;p&gt;所以最后在Header Editor里实现个替换脚本就很简单了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return val
  .replaceAll(&quot;// item?.reply_control?.location ? `&amp;lt;span class=\&quot;reply-location\&quot;&amp;gt;IP属地：${item?.reply_control?.location || &apos;&apos;}&amp;lt;/span&amp;gt;` : &apos;&apos;,&quot;,
    &quot;item?.reply_control?.location ? `&amp;lt;span style=\&quot;margin-left:5px; color: #0CC;\&quot;&amp;gt;${item?.reply_control?.location || &apos;&apos;}&amp;lt;/span&amp;gt;` : &apos;&apos;,item?.invisible ? &apos;&amp;lt;span style=\&quot;margin-left: 5px;color: red;\&quot;&amp;gt;评论被隐藏&amp;lt;/span&amp;gt;&apos; : &apos;&apos;,&quot;
  )
  .replaceAll(
    &quot;item.invisible&quot;,
    &quot;false&quot;
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是网页端旧版评论区逆向的原理了&lt;/p&gt;
&lt;h2&gt;新版评论区逆向&lt;/h2&gt;
&lt;p&gt;就在我整完旧版评论区逆向发布脚本后，有群友跟我说不能用，F12抓网络也只能找到个&lt;code&gt;comment-pc-vue.next.js&lt;/code&gt;的东西。经我测试发现B站网页端的旧版和新版界面竟然用的不是同一套处理方案，新版的是以vue为基础整的，并且这个&lt;code&gt;comment-pc-vue.next.js&lt;/code&gt;虽然名字里没有min但确实是给压缩完了。幸好有前面逆向旧版评论区的经验，我猜测新版评论区也保留了&lt;code&gt;reply-location&lt;/code&gt;等一系列和IP属地相关的样式等，所以可以按类似的方法分析&lt;/p&gt;
&lt;h3&gt;获取js源码&lt;/h3&gt;
&lt;p&gt;首先要做的自然是获取相关脚本的源码了
B站新版界面前端的源码似乎还处于开发中，大概每隔几天就会重新打包更新一次，这里以2023/3/5的版本为例&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://webtest.lq0.tech/comment-pc-vue.next.js&quot;&gt;源码&lt;/a&gt;
&lt;a href=&quot;https://webtest.lq0.tech/comment-pc-vue.next.formatted.js&quot;&gt;Firefox开发者工具格式化后的源码&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;从&lt;a href=&quot;https://s1.hdslb.com/bfs/seed/jinkela/commentpc/comment-pc-vue.next.js&quot;&gt;https://s1.hdslb.com/bfs/seed/jinkela/commentpc/comment-pc-vue.next.js&lt;/a&gt;把脚本下载下来看看&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;comment-pc-vue.next.js.png&quot; alt=&quot;2023/3/5&quot; /&gt;&lt;/p&gt;
&lt;p&gt;彳亍，这可是真的是.min.js了。不过问题不大，先用Firefox对代码进行格式化并保存后再来找找&lt;code&gt;reply-location&lt;/code&gt;看看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 第44279行
os = {
  key: 0,
  class : &apos;reply-location&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;搜索出来能搜出好几个，这里明显能看出这段代码是把&lt;code&gt;reply-location&lt;/code&gt;样式给整进变量&lt;code&gt;os&lt;/code&gt;里了，那我们来看看&lt;code&gt;os&lt;/code&gt;在哪里被引用了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 44714行
(0, P._) (&apos;div&apos;, ts, [
    (0, P._) (&apos;span&apos;, rs, (0, kt.toDisplayString) ((null === (a = (0, z.SU) (m)) || void 0 === a ? void 0 : a.last_mtime_text) || (0, z.SU) (lt) ((0, z.SU) (v))), 1),
    (0, z.SU) (f) ? ((0, P.wg) (), (0, P.iD) (&apos;span&apos;, os, (0, kt.toDisplayString) ((0, z.SU) (f)), 1)) : (0, P.kq) (&apos;v-if&apos;, !0),
    (0, P._) (&apos;span&apos;, {
        class : &apos;reply-like&apos;,
        onClick: ke
    }, ...
]),
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找是找出来了，可是这坨代码是nm啥啊（&lt;/p&gt;
&lt;p&gt;先别急，一步步来分析&lt;/p&gt;
&lt;h3&gt;源码分析&lt;/h3&gt;
&lt;p&gt;首先我们可以注意到，这段代码里有很多字符串常量，如&lt;code&gt;div&lt;/code&gt;、&lt;code&gt;span&lt;/code&gt;、&lt;code&gt;reply-like&lt;/code&gt;等。再加上我们刚刚找到的&lt;code&gt;os&lt;/code&gt;变量，可以合理猜测这里就是在创建评论区对应的DOM元素&lt;/p&gt;
&lt;p&gt;再注意一下每行代码的开头&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(0, P._)
(0, P._)
(0, z.SU)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不熟悉JavaScript特性的话可能就不知道这段是在整啥，这个其实是在使用JavaScript的逗号表达式，会返回最后一个表达式的值，详见&lt;a href=&quot;https://wangdoc.com/javascript/operators/priority#%E9%80%97%E5%8F%B7%E8%BF%90%E7%AE%97%E7%AC%A6&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;所以这里其实就是等于直接取后面的值，也就是&lt;code&gt;P._&lt;/code&gt;和&lt;code&gt;z.SU&lt;/code&gt;。在这几个后面直接就跟了括号，所以这里其实就是取&lt;code&gt;P._&lt;/code&gt;和&lt;code&gt;z.SU&lt;/code&gt;，并把它们当作函数进行调用
为什么是&lt;code&gt;P._&lt;/code&gt;、&lt;code&gt;z.SU&lt;/code&gt;这种鬼畜的变量名？这是打包工具对源码进行压缩的结果，就当作是混淆后的变量名得了。因为是经过打包压缩的代码，所以我们这里就不指望能通过删注释就复原IP属地显示了，我们得自己写一个出来&lt;/p&gt;
&lt;p&gt;至于为什么不直接写&lt;code&gt;P._()&lt;/code&gt;进行调用，而要写&lt;code&gt;(0, P._) ()&lt;/code&gt;这种鬼畜的形式？这是为了改变函数里的&lt;code&gt;this&lt;/code&gt;指向，可以看看&lt;a href=&quot;https://blog.shiinafan.top/posts/%E4%B8%80%E4%B8%AA%E8%BE%83%E5%86%B7%E9%97%A8%E7%9A%84JS%E7%9F%A5%E8%AF%86/&quot;&gt;隔壁&lt;/a&gt;的讲解，这里不再赘述&lt;/p&gt;
&lt;p&gt;所以这里摆明了就是函数调用，包括后面的&lt;code&gt;(0, P.wg)&lt;/code&gt;、&lt;code&gt;(0, P.iD)&lt;/code&gt;、&lt;code&gt;(0, kt.toDisplayString)&lt;/code&gt;、&lt;code&gt;(0, z.SU)&lt;/code&gt;、&lt;code&gt;(0, P.kq)&lt;/code&gt;全都是一个意思，都是在进行函数调用而已&lt;/p&gt;
&lt;p&gt;接下来尝试分析其中的一行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(0, P._) (&apos;span&apos;, rs, (0, kt.toDisplayString) ((null === (a = (0, z.SU) (m)) || void 0 === a ? void 0 : a.last_mtime_text) || (0, z.SU) (lt) ((0, z.SU) (v))), 1),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先别两眼一黑，一步步来
我们知道&lt;code&gt;P._&lt;/code&gt;就是一个函数，先不管这个函数具体是啥，一个个来看后面的参数&lt;/p&gt;
&lt;p&gt;第一个参数&lt;code&gt;&apos;span&apos;&lt;/code&gt;很明显就是指的&lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;标签&lt;/p&gt;
&lt;p&gt;第二个参数&lt;code&gt;rs&lt;/code&gt;我们可以查找一下引用，不难发现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 44276行
rs = {
    class : &apos;reply-time&apos;
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好，这个明显就是评论时间标签的样式。注意&lt;code&gt;rs&lt;/code&gt;赋值的方法是上述代码，而不是&lt;code&gt;rs = &apos;reply-time&apos;,&lt;/code&gt;，所以我们可以猜测&lt;code&gt;rs&lt;/code&gt;和上文提到的&lt;code&gt;os&lt;/code&gt;都不止是&lt;code&gt;class&lt;/code&gt;，而是这个元素的所有属性，包括&lt;code&gt;class&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;继续看下一个参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(0, kt.toDisplayString) ((null === (a = (0, z.SU) (m)) || void 0 === a ? void 0 : a.last_mtime_text) || (0, z.SU) (lt) ((0, z.SU) (v)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;？？？
别急，还是一步步来&lt;/p&gt;
&lt;p&gt;首先，&lt;code&gt;kt.toDisplayString&lt;/code&gt;很明显就是个把后面的东西转换成字符串的函数，那我们直接看后面的参数...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(null === (a = (0, z.SU) (m)) || void 0 === a ? void 0 : a.last_mtime_text) || (0, z.SU) (lt) ((0, z.SU) (v))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这nm是啥？&lt;/p&gt;
&lt;p&gt;说实话，我逆向到这的时候我也不知道这是个锤子，但后面能看到几个有意义的参数，如&lt;code&gt;(0, z.SU) (m)&lt;/code&gt;、&lt;code&gt;a.last_mtime_text&lt;/code&gt;、&lt;code&gt;(0, z.SU) (lt) ((0, z.SU) (v))&lt;/code&gt;等&lt;/p&gt;
&lt;p&gt;前面两个提供不了什么特别有用的信息，来看看第三个&lt;code&gt;(0, z.SU) (lt) ((0, z.SU) (v))&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;查看一下&lt;code&gt;lt&lt;/code&gt;的引用，可以找到它的定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 37575行
lt = function (e) {
    var n = Math.floor((new Date).getTime() / 1000) - e,
    ...
    if (n &amp;lt; 60) {
        var v = 10 * Math.floor(n % 60 / 10);
        return v &amp;gt;= 10 ? &apos;&apos;.concat(v, &apos;秒前&apos;) : &apos;刚刚&apos;
    }
    if (60 &amp;lt;= n &amp;amp;&amp;amp; n &amp;lt; 3600) {
        var f = Math.floor(n / 60);
        return &apos;&apos;.concat(f, &apos;分钟前&apos;)
    }
    if (n &amp;gt;= 3600) {
        var A = Math.floor(n / 3600);
        return &apos;&apos;.concat(A, &apos;小时前&apos;)
    }
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一眼顶针，鉴定为根据评论时间计算该显示的字符串
所以这个函数的参数&lt;code&gt;e&lt;/code&gt;就是后面的&lt;code&gt;(0, z.SU) (v)&lt;/code&gt;了&lt;/p&gt;
&lt;p&gt;我们来看看&lt;code&gt;v&lt;/code&gt;的定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 44345行
v = p.replyTime,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这名字怎么看都像是评论的具体时间&lt;/p&gt;
&lt;p&gt;因此，这里我们可以简单猜测一下这行代码的作用&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;P._&lt;/code&gt;是创建HTML元素用的函数，它接收三个参数，第一个是字符串类型的元素名称，第二个是元素的各种属性，第三个估计不是&lt;code&gt;innerText&lt;/code&gt;就是&lt;code&gt;innerHTML&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;z.SU&lt;/code&gt;是某种取值函数，像是这里的&lt;code&gt;lt&lt;/code&gt;和&lt;code&gt;v&lt;/code&gt;就是和评论时间相关的函数/变量&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;（这里其实可以猜测&lt;code&gt;P._&lt;/code&gt;就是vue的渲染函数&lt;code&gt;h()&lt;/code&gt;，详见&lt;a href=&quot;https://cn.vuejs.org/api/render-function.html#h&quot;&gt;这里&lt;/a&gt;，可以看到参数类型跟这里的一模一样）&lt;/p&gt;
&lt;p&gt;既然得知了这行代码的大致作用，我们可以修改源码输出一下&lt;code&gt;v&lt;/code&gt;试试，将&lt;code&gt;(0, z.SU) (lt) ((0, z.SU) (v))&lt;/code&gt;直接整成&lt;code&gt;console.log(v)&lt;/code&gt;，会发现&lt;code&gt;v&lt;/code&gt;就是vue的响应式对象&lt;/p&gt;
&lt;p&gt;那&lt;code&gt;(0, z.SU)&lt;/code&gt;的作用就很明显了，就是对响应式对象进行&lt;code&gt;.value&lt;/code&gt;取值&lt;/p&gt;
&lt;p&gt;接下来我们可以看看下一行代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(0, z.SU) (f) ? ((0, P.wg) (), (0, P.iD) (&apos;span&apos;, os, (0, kt.toDisplayString) ((0, z.SU) (f)), 1)) : (0, P.kq) (&apos;v-if&apos;, !0),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好，这nm又是什么（&lt;/p&gt;
&lt;p&gt;别急，开头的&lt;code&gt;f&lt;/code&gt;我们可以先查找下引用，不难发现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 44346行
f = p.replyLocation,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这很有可能就是没有被显示出来的IP属地
后面跟的是一个三元表达式，既然当&lt;code&gt;f&lt;/code&gt;为&lt;code&gt;false&lt;/code&gt;时显示&lt;code&gt;(0, P.kq) (&apos;v-if&apos;, !0)&lt;/code&gt;，那就当这段代码是&lt;code&gt;v-if&lt;/code&gt;判断得了，结果应该就是不创建元素&lt;/p&gt;
&lt;p&gt;所以我们把重点放到前面来，可以看到关键代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(0, P.iD) (&apos;span&apos;, os, (0, kt.toDisplayString) ((0, z.SU) (f)), 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;(0, P.iD)&lt;/code&gt;和前文类似，一眼顶针鉴定为创建元素
后面的三个参数一样，&lt;code&gt;os&lt;/code&gt;就是刚刚的&lt;code&gt;reply-location&lt;/code&gt;，即IP属地的显示样式。后面的&lt;code&gt;(0, z.SU)&lt;/code&gt;就是前面说的对响应式对象取值&lt;/p&gt;
&lt;p&gt;我当时逆向时是在这里把前面的&lt;code&gt;(f)&lt;/code&gt;改成&lt;code&gt;(true)&lt;/code&gt;强制让它输出&lt;code&gt;f&lt;/code&gt;的内容，结果发现&lt;code&gt;f&lt;/code&gt;恒为&lt;code&gt;false&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;上文提到&lt;code&gt;f = p.replyLocation&lt;/code&gt;，那我们看看这个&lt;code&gt;p&lt;/code&gt;是怎么回事&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 44343行
p = xt(t),
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 37905行
xt = function (e) {
    var n = (0, P.Fl) ((function () {
        var n;
        return (null === (n = e.value) || void 0 === n ? void 0 : n.rpid) || 0
    })),
    t = (0, P.Fl) ((function () {
        var n;
        return (null === (n = e.value) || void 0 === n ? void 0 : n.root) || 0
    })),

    ...

    return {
        replyId: n,
        rootReplyId: t,
        replyForReplyId: r,
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很明显&lt;code&gt;xt&lt;/code&gt;就是把评论数据解析成对象的函数，来看其中一个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var n = (0, P.Fl) ((function () {
    var n;
    return (null === (n = e.value) || void 0 === n ? void 0 : n.rpid) || 0
})),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里就是在解析一个属性了。注意看&lt;code&gt;n.rpid&lt;/code&gt;，这个是后端返回的评论原始JSON数据里的一个属性
&lt;code&gt;P.Fl&lt;/code&gt;应该就是vue的&lt;code&gt;ref&lt;/code&gt;了，因为返回的值是vue的响应式对象，需要我们使用&lt;code&gt;.value&lt;/code&gt;才能获取具体的值
前面那一段可以猜测是打包工具针对不支持ES6的浏览器写的polyfill，猜测源码应该是使用&lt;a href=&quot;https://wangdoc.com/es6/operator#%E9%93%BE%E5%88%A4%E6%96%AD%E8%BF%90%E7%AE%97%E7%AC%A6&quot;&gt;链判断运算符&lt;/a&gt;写的空值判断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let n = ref(e.value?.rpid ?? 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此可以猜测后面每一个&lt;code&gt;x = (0, P.Fl) ...&lt;/code&gt;都是在读取一个属性，最后的return就是把这些读取的单个属性拼接成一个对象
那我们就来看看最后返回的&lt;code&gt;replyLocation&lt;/code&gt;到底是怎么回事&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 38172行
replyLocation: l,
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 37926行
l = (0, P.Fl) ((function () {
    return !1
})),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里要提一下js的自动类型转换，&lt;code&gt;!1&lt;/code&gt;是打包工具对于&lt;code&gt;false&lt;/code&gt;的简写，在比较前会自动转换为&lt;code&gt;false&lt;/code&gt;，所以&lt;code&gt;!1 === false&lt;/code&gt;
所以我们可以合理猜测源码是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let l = ref(false);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;合着location是tm固定返回false啊（恼）&lt;/p&gt;
&lt;p&gt;我们还有一个功能是标记隐藏评论，这个就在刚刚&lt;code&gt;p = xt(t)&lt;/code&gt;的下面，不难发现&lt;code&gt;C = p.invisible&lt;/code&gt;，创建新元素的时候判断一下这个&lt;code&gt;C&lt;/code&gt;，并查找下&lt;code&gt;C&lt;/code&gt;的引用把原先的判断条件全改成&lt;code&gt;false&lt;/code&gt;就行了（&lt;/p&gt;
&lt;p&gt;现在我们把需要了解的源码都基本上浏览了一遍，可以开始魔改了&lt;/p&gt;
&lt;h3&gt;源码修改&lt;/h3&gt;
&lt;p&gt;我们从44716行开始魔改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(0, z.SU) (f) ? ((0, P.wg) (), (0, P.iD) (&apos;span&apos;, os, (0, kt.toDisplayString) ((0, z.SU) (f)), 1)) : (0, P.kq) (&apos;v-if&apos;, !0),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行的作用是创建IP属地显示元素，若存在就显示，不存在就不创建&lt;/p&gt;
&lt;p&gt;按照惯例我们先改一下它的样式，把&lt;code&gt;os&lt;/code&gt;改成我们自己的&lt;code&gt;{class:&quot;reply-location&quot;,style:{color:&quot;#0CC&quot;}}&lt;/code&gt;，这样就能在保持原有样式的同时加上我们自己的字体颜色显示了&lt;/p&gt;
&lt;p&gt;源码里已经把创建元素都写好了，改的是收到的IP属地的数据，所以我们这里不需要动创建元素这里，直接取改原数据处理那一段
接下来我们就需要改&lt;code&gt;f&lt;/code&gt;，即&lt;code&gt;replyLocation&lt;/code&gt;了&lt;/p&gt;
&lt;p&gt;按照上文的分析，&lt;code&gt;replyLocation&lt;/code&gt;本身就被设计成固定返回&lt;code&gt;false&lt;/code&gt;，我们得直接修改它的值
不过按照上文的分析也不难实现，只要把37927行的&lt;code&gt;return !1&lt;/code&gt;改成&lt;code&gt;return e.value?.reply_control?.location&lt;/code&gt;即可，这样就可以正确处理IP属地了&lt;/p&gt;
&lt;p&gt;下一行我们得创建自己的隐藏评论标记元素，已知&lt;code&gt;C&lt;/code&gt;是ref(评论是否被隐藏)，&lt;code&gt;P.iD&lt;/code&gt;是创建元素，直接照抄上面创建元素的方法即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(C.value)?((0,P.iD)(&quot;span&quot;,{style:{marginLeft:&quot;-10px&quot;,marginRight:&quot;10px&quot;,color:&quot;red&quot;}},(0,kt.toDisplayString)(&quot;评论被隐藏&quot;),1)):(0,P.iD)(&quot;span&quot;,{style:{display:&quot;none&quot;}}),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的逻辑是，先取&lt;code&gt;C.value&lt;/code&gt;判断评论是否被隐藏，如果是就在IP属地元素后创建个自己的&lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;，设置好样式，内容就是&lt;code&gt;&quot;评论被隐藏&quot;&lt;/code&gt;，否则就创建个不显示的空元素（我不知道怎么在不创建元素的情况下还不报错，只能这么弄了）&lt;/p&gt;
&lt;p&gt;最后别忘了查找对于&lt;code&gt;C&lt;/code&gt;的引用并把判断条件改成&lt;code&gt;false&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 44556行
return (0, z.SU) (w) || (0, z.SU) (C) ? (0, P.kq) (&apos;v-if&apos;, !0) : ((0, P.wg) (), (0, P.iD) (&apos;div&apos;, { ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接把&lt;code&gt;(C)&lt;/code&gt;改成&lt;code&gt;(false)&lt;/code&gt;即可&lt;/p&gt;
&lt;p&gt;这样就完成主楼回复内容替换了&lt;/p&gt;
&lt;p&gt;但别忘了上文说的，除了主楼之外还有楼中楼也得进行相同的处理，楼中楼对应的样式是&lt;code&gt;sub-reply-location&lt;/code&gt;，按和上文完全相同的流程再走一遍即可&lt;/p&gt;
&lt;h3&gt;通用替换脚本&lt;/h3&gt;
&lt;p&gt;按以上方法确实可以实现所需的功能，但是没法整出通用替换脚本，因为B站前端新版界面似乎一直在开发，每几天就会重新打包一次，很多的变量名都会改
所以得用一些方法在变量名被修改时也能匹配对应的代码&lt;/p&gt;
&lt;p&gt;我用的方法是正则表达式配合捕获组
虽然匹配的方法有亿点复杂吧（&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//主楼invisible变量
let replyInvisible = val.match(/(return\(0,\w+\.\w+\)\(\w\)\|\|\(0,\w+\.\w+\)\()(\w)(\)\?\(0,\w+\.\w+\)\(&quot;v-if&quot;,!0\):\(\(0,\w\.\w+\)\(\),\(0,\w+\.\w+\)\(&quot;div&quot;,{key:0,class:&quot;reply)/)?.[2];
//回复楼invisible变量
let subReplyInvisible = val.match(/(return\(0,\w+\.\w+\)\(\w\)\|\|\(0,\w+\.\w+\)\()(\w)(\)\?\(0,\w+\.\w+\)\(&quot;v-if&quot;,!0\):\(\(0,\w\.\w+\)\(\),\(0,\w+\.\w+\)\(&quot;div&quot;,{key:0,class:\()/)?.[2];

//隐藏评论恢复显示
val = val
    .replace(`(${replyInvisible})?(0`, &quot;(false)?(0&quot;)
    .replace(`(${subReplyInvisible})?(0`, &quot;(false)?(0&quot;);

//主楼IP属地样式reply-location
let classReplyLocation = val.match(/(\w\w)={key:0,class:&quot;reply-location&quot;}/)?.[1];
//回复楼IP属地样式sub-reply-location
let classSubReplyLocation = val.match(/(\w\w)={key:0,class:&quot;sub-reply-location&quot;}/)?.[1];

//主楼元素创建匹配正则
let repRegExp = new RegExp(`(\\(0,\\w+\\.\\w+\\)\\(\\w\\)\\?\\(\\(0,\\w+\\.\\w+\\)\\(\\),\\(0,)(\\w+\\.\\w+)(\\)\\(&quot;span&quot;,)(${classReplyLocation})(,\\(0,)(\\w+)(\\.toDisplayString\\)\\(\\(0,\\w+\\.\\w+\\)\\(\\w\\)\\),1\\)\\):\\(0,\\w+\\.\\w+\\)\\(&quot;v-if&quot;,!0\\),)`);
//回复楼元素创建匹配正则
let subRepRegExp = new RegExp(`(\\(0,\\w+\\.\\w+\\)\\(\\w\\)\\?\\(\\(0,\\w+\\.\\w+\\)\\(\\),\\(0,)(\\w+\\.\\w+)(\\)\\(&quot;span&quot;,)(${classSubReplyLocation})(,\\(0,)(\\w+)(\\.toDisplayString\\)\\(\\(0,\\w+\\.\\w+\\)\\(\\w\\)\\),1\\)\\):\\(0,\\w+\\.\\w+\\)\\(&quot;v-if&quot;,!0\\),)`);

let createElement = val.match(repRegExp)?.[2];
let toDisplayStringVar = val.match(repRegExp)?.[6];

val = val
    .replace(repRegExp,
        //主楼IP属地显示
        &apos;$1$2$3{class:&quot;reply-location&quot;,style:{color:&quot;#0CC&quot;}}$5$6$7&apos; +
        //主楼隐藏评论标记
        `(${replyInvisible}.value)?((0,${createElement})(&quot;span&quot;,{style:{marginLeft:&quot;-10px&quot;,marginRight:&quot;10px&quot;,color:&quot;red&quot;}},(0,${toDisplayStringVar}.toDisplayString)(&quot;该评论被隐藏&quot;),1)):(0,${createElement})(&quot;span&quot;,{style:{display:&quot;none&quot;}}),`)
    .replace(subRepRegExp,
        //回复楼IP属地显示
        &apos;$1$2$3{class:&quot;sub-reply-location&quot;,style:{color:&quot;#0CC&quot;}}$5$6$7&apos; +
        //回复楼隐藏评论标记
        `(${subReplyInvisible}.value)?((0,${createElement})(&quot;span&quot;,{style:{marginLeft:&quot;-10px&quot;,marginRight:&quot;10px&quot;,color:&quot;red&quot;}},(0,${toDisplayStringVar}.toDisplayString)(&quot;评论被隐藏&quot;),1)):(0,${createElement})(&quot;span&quot;,{style:{display:&quot;none&quot;}}),`);

val = val
    // location唯一替换，IP属地还原
    .replace(/(\w=\(0,\w+\.\w+\)\(\(function\(\){return)(!1)(}\)\))/, &quot;$1 e.value?.reply_control?.location||false$3&quot;);

return val;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正则表达式的相关内容这里不再叙述，这边匹配的原理就是匹配各段代码之间的不同处理逻辑，而不是变量名，变量名会因为打包工具随便更改，所以我直接用的&lt;code&gt;\w+\.\w+&lt;/code&gt;进行匹配，但基础的处理逻辑是基本不会改的&lt;/p&gt;
&lt;p&gt;提取指定变量我是直接取的捕获组，用括号区分各个捕获组之后再用&lt;code&gt;match&lt;/code&gt;进行匹配，可以直接以数组下标的形式把匹配到的内容提取出来
用正则表达式替换的时候也可以使用捕获组，在替换的内容里写&lt;code&gt;$n&lt;/code&gt;就可以使用第&lt;code&gt;n&lt;/code&gt;个捕获组里的文本内容&lt;/p&gt;
&lt;p&gt;&lt;s&gt;所以这里为了提取各种变量最多甚至用到了多达7个捕获组&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;我是在&lt;a href=&quot;https://regex101.com/&quot;&gt;这里&lt;/a&gt;测试这些超级复杂的正则表达式的，这网站甚至会帮你把捕获组标出来，还能测试替换后的结果&lt;/p&gt;
&lt;h2&gt;个人空间关注/粉丝数查看上限突破+关注时间显示&lt;/h2&gt;
&lt;p&gt;讲真这里的逆向比刚刚评论区逆向简单得多，我就不（lan）用（de）放图演示了，直接口述吧&lt;/p&gt;
&lt;p&gt;首先F12抓包粉丝/关注显示可以发现，get参数里有个&lt;code&gt;ps=20&lt;/code&gt;，这个参数就是控制每页显示多少个人的。经测试可以发现最大可以设置为50。由于访问页数还是只有5页，因此针对其他用户，可查看的粉丝/关注数就由原先的&lt;code&gt;5 * 20 = 100&lt;/code&gt;个提升为&lt;code&gt;5 * 50 = 250&lt;/code&gt;个&lt;/p&gt;
&lt;p&gt;既然参数里有ps，那可以合理猜测发起请求时使用了一个包含&lt;code&gt;ps&lt;/code&gt;属性的js对象作为参数，简单搜索&lt;code&gt;ps:20&lt;/code&gt;即可发现相应的源码，直接查找替换成&lt;code&gt;ps:50&lt;/code&gt;即可&lt;/p&gt;
&lt;p&gt;至于页数计算，查看源码可知是由&lt;code&gt;Math.ceil(this.curTag.count/50)&lt;/code&gt;写死的......&lt;/p&gt;
&lt;p&gt;那就直接把&lt;code&gt;/20&lt;/code&gt;替换成&lt;code&gt;/50&lt;/code&gt;即可&lt;/p&gt;
&lt;p&gt;关注时间显示这块，照例直接找关注界面的css class属性，可以很快就找到这部分源码。查看源码可知变量&lt;code&gt;i&lt;/code&gt;很可能是粉丝对象本身，配合抓包内容可以猜测&lt;code&gt;i.mtime&lt;/code&gt;就是关注时间。模仿前面的创建元素的代码，使用&lt;code&gt;t._v(t._s())&lt;/code&gt;即可创建元素。注意对于部分较早期关注的粉丝，关注时间可能是不存在的，因此记得做空值判断&lt;/p&gt;
&lt;h2&gt;相簿界面发布时间显示&lt;/h2&gt;
&lt;p&gt;一样开局根据css的class属性直接找相关代码，比如可以直接搜查看次数的图标的class&lt;code&gt;album-card__count-icon--view&lt;/code&gt;即可直接找到对应代码。同空间相关代码，可以猜测&lt;code&gt;t.item&lt;/code&gt;就是每个相簿元素本身，根据抓包结果可以猜测&lt;code&gt;t.item.ctime&lt;/code&gt;就是发布时间。模仿上文代码，使用&lt;code&gt;e()&lt;/code&gt;即可创建元素。这里要注意，根据前文代码的&lt;code&gt;staticClass&lt;/code&gt;，搜索一下可知这是vue的创建元素的参数。如果我们想添加内嵌样式，得使用&lt;code&gt;staticStyle&lt;/code&gt;，而这个参数需要传递一个对象。最后，第三个数组就是元素的内部内容，同样仿造前文使用&lt;code&gt;t._v(t._s())&lt;/code&gt;传入一个字符串即可创建对应元素，我这直接使用了&lt;code&gt;new Date(t.item.ctime * 1000).toLocaleString()&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;以上就是本插件的主要原理讲解。由于该插件的原理是直接修改b站的前端源码，因此我能想到的通用方案就只有使用能重定向请求的插件配合CDN实现了。希望以上原理讲解能给你的开发带来或多或少的帮助&lt;/p&gt;
</content:encoded></item><item><title>BilibiliEnhance - B站功能增强</title><link>https://lq0.tech/posts/bilibilienhance/</link><guid isPermaLink="true">https://lq0.tech/posts/bilibilienhance/</guid><description>增强B站网页端功能，显示IP属地、隐藏评论、关注时间等</description><pubDate>Fri, 24 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;简介&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;注：该项目已不再维护，请移步至&lt;a href=&quot;https://github.com/the1812/Bilibili-Evolved&quot;&gt;Bilibili Evolved&lt;/a&gt;搜索&lt;code&gt;IP属地显示&lt;/code&gt;、&lt;code&gt;关注时间显示&lt;/code&gt;、&lt;code&gt;相簿发布时间显示&lt;/code&gt;等组件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/LightQuanta/BilibiliEnhance&quot;&gt;https://github.com/LightQuanta/BilibiliEnhance&lt;/a&gt;
使用&lt;a href=&quot;http://he.firefoxcn.net/&quot;&gt;Header Editor&lt;/a&gt;增强b站部分功能&lt;/p&gt;
&lt;h1&gt;功能列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;在b站网页端显示IP属地&lt;/li&gt;
&lt;li&gt;显示被隐藏的评论&lt;/li&gt;
&lt;li&gt;关注数/粉丝数列表查看从每页可查看20个增加至50个&lt;/li&gt;
&lt;li&gt;关注数/粉丝数列表可查看关注的具体时间&lt;/li&gt;
&lt;li&gt;相簿界面可以看到发布的具体时间&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;效果图&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;15.png&quot; alt=&quot;评论增强显示&quot; /&gt;
&lt;img src=&quot;subtimetest.png&quot; alt=&quot;关注/粉丝列表增强显示&quot; /&gt;
&lt;img src=&quot;albumpubtime.png&quot; alt=&quot;相簿界面发布时间显示&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;使用教程&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;下载浏览器插件&lt;a href=&quot;http://he.firefoxcn.net/&quot;&gt;Header Editor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;转到导出和导入界面，链接填写&lt;a href=&quot;https://cdn.jsdelivr.net/gh/LightQuanta/BilibiliEnhance@1.1.6/BilibiliEnhance.json&quot;&gt;https://cdn.jsdelivr.net/gh/LightQuanta/BilibiliEnhance@1.1.6/BilibiliEnhance.json&lt;/a&gt; ，保存至任意分组并启用规则即可&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;遇到问题请先尝试重新导入最新规则，若还是不行再联系Light_Quanta反馈问题
上次更新：2023/5/13 22:08:22&lt;/p&gt;
&lt;h1&gt;原理&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://lq0.tech/2023/02/27/bilicommentreverse/&quot;&gt;看这里&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;联动项目&lt;/h1&gt;
&lt;p&gt;隔壁群u通过直接修改vue属性在新版ui实现的类似功能
&lt;a href=&quot;https://github.com/QingXia-Ela/unlock-bilibili-PcBrowser-comment&quot;&gt;unlock-bilibili-PcBrowser-comment&lt;/a&gt;
&lt;a href=&quot;https://greasyfork.org/zh-CN/scripts/460488-bilibili-%E5%B1%95%E7%A4%BA%E8%AF%84%E8%AE%BAip%E5%B1%9E%E5%9C%B0%E4%B8%8E%E6%98%BE%E7%A4%BA%E8%A2%AB%E9%9A%90%E8%97%8F%E8%AF%84%E8%AE%BA&quot;&gt;油猴脚本&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>常见的各种链接都是什么格式——URL科普</title><link>https://lq0.tech/posts/url/</link><guid isPermaLink="true">https://lq0.tech/posts/url/</guid><description>介绍URL的格式和如何安全地缩短链接</description><pubDate>Thu, 28 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;平常我们总是能见到各种各样的链接，比如&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://www.bilibili.com/video/BV1GJ411x7h7?mid=114514&amp;amp;p=1&amp;amp;share_from=ugc&amp;amp;share_medium=android&amp;amp;share_plat=android&amp;amp;share_source=COPY&amp;amp;share_tag=s_i&amp;amp;timestamp=1658778647&amp;amp;unique_k=8yqWp46&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://music.163.com/song?id=18520488&amp;amp;userid=1919810&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;steam://install/1172470&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;等等。这些链接往往都很长又非常复杂，而且一些链接里还带跟踪参数什么的，有可能会泄露出一些你不想泄露的个人信息。本文将介绍平常见到的各种链接的具体格式，以及如何删除链接中的无用参数和跟踪参数，从而缩短你的链接&lt;/p&gt;
&lt;h1&gt;URL协议&lt;/h1&gt;
&lt;p&gt;我们平常看到的各种链接几乎全都是URL格式的。URL的全称是&lt;strong&gt;U&lt;/strong&gt;niform &lt;strong&gt;R&lt;/strong&gt;esource &lt;strong&gt;L&lt;/strong&gt;ocator（统一资源定位符）。在讲解URL之前，我们需要先了解一下为什么我们需要URL。我们在分享链接的时候是在分享什么？一个指定的网络资源的位置。我们通过链接来确定我们要获取的资源的地址，从而让我们的各种软件可以找到我们需要的资源
URL就是一个表示资源地址和获取方法的协议。如同我们描述一个地理位置一样，我们可能会用&lt;code&gt;国家-省-市-区-道路编号&lt;/code&gt;这种格式来描述一个地址。当我们表示一个网上的资源的地址的时候，我们也需要一种格式来表示一个地址，这个格式就是URL&lt;/p&gt;
&lt;p&gt;URL的格式基本上是这样的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;lt;scheme&amp;gt;://&amp;lt;user&amp;gt;:&amp;lt;password&amp;gt;@&amp;lt;host&amp;gt;:&amp;lt;port&amp;gt;/&amp;lt;url-path&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;是不是有亿点复杂？没关系，这个是完整的格式，其中很多参数都是可选的。我们一个个参数来看&lt;/p&gt;
&lt;p&gt;scheme： 连接地址所需的协议。我们最常见的协议就是http(s)协议。http(s)协议用于访问网页&lt;/p&gt;
&lt;p&gt;user@password：这个参数比较少见，是个可选参数，用于在连接时附加用户名和密码，常见的连接里都是没有这个的，知道一下就行&lt;/p&gt;
&lt;p&gt;host:port： 表示要访问的服务器的地址和端口，常见的连接里也不会写端口，直接就是个host&lt;/p&gt;
&lt;p&gt;/url-path： 这段就是要访问的文件的具体地址。如果说host指的是要找哪个大楼的话，那url-path就是大楼里的具体门牌号。知道要访问的文件的具体路径才能最终帮助我们找到文件&lt;/p&gt;
&lt;p&gt;我们先以http(s)为例&lt;/p&gt;
&lt;h2&gt;http(s)链接的格式&lt;/h2&gt;
&lt;p&gt;以这个连接为例
&lt;code&gt;https://www.bilibili.com/video/online.html&lt;/code&gt;
我们拆分开来看&lt;/p&gt;
&lt;p&gt;https://： 表示访问这个连接时要使用访问网页的https协议
www.bilibili.com： 表示要访问的服务器的地址，这里是b站的网页服务器的地址&lt;/p&gt;
&lt;p&gt;/video/online.html： 要访问的文件的具体地址，可以理解成访问的是/video目录下的online.html文件&lt;/p&gt;
&lt;p&gt;这就是个简单的连接的基本结构&lt;/p&gt;
&lt;p&gt;但是等等，我们平常访问的连接好像没这么简单啊？在url-pattern后面为什么会有个问号？文件名里不是不允许有问号的吗？而且后面还跟了一大堆莫名其妙的参数。这些都是可选的查询参数&lt;/p&gt;
&lt;h3&gt;查询参数&lt;/h3&gt;
&lt;p&gt;查询参数可以在你访问你要的文件的时候提供一些&lt;strong&gt;额外信息&lt;/strong&gt;，比如需要的文件格式等。这种参数的基本格式是&lt;code&gt;?参数1=值1&amp;amp;参数2=值2&amp;amp;参数3=值3...&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;以上文的&lt;code&gt;https://music.163.com/song?id=18520488&amp;amp;userid=1919810&lt;/code&gt;为例，这个链接里跟了两个查询参数&lt;code&gt;id&lt;/code&gt;和&lt;code&gt;userid&lt;/code&gt;，值分别是&lt;code&gt;18520488&lt;/code&gt;和&lt;code&gt;1919810&lt;/code&gt;。网易云音乐在你访问音乐播放页面的时候访问的连接其实一直都是&lt;code&gt;https://music.163.com/song&lt;/code&gt;，具体播放哪首歌由后面的查询参数&lt;code&gt;id=18520488&lt;/code&gt;决定, 若不指定则无法播放。后面的&lt;code&gt;userid=1919810&lt;/code&gt;并不会影响播放，只是指定分享这首歌的用户的id。&lt;/p&gt;
&lt;p&gt;知道了这一点后，我们就可以缩短链接并删除不需要的查询参数了。以&lt;code&gt;https://www.bilibili.com/video/BV1GJ411x7h7?mid=114514&amp;amp;p=1&amp;amp;share_from=ugc&amp;amp;share_medium=android&amp;amp;share_plat=android&amp;amp;share_source=COPY&amp;amp;share_tag=s_i&amp;amp;timestamp=1658778647&amp;amp;unique_k=8yqWp46&lt;/code&gt;为例，这个链接长得一匹，后面还跟了一大堆可能会泄露个人信息的跟踪参数。b站的视频是根据BV号来确定的，所以这里的所有跟踪参数对于视频播放都是没有影响的，所以问号及后面的查询参数全都可以安全删除，我们就可以把链接缩短成&lt;code&gt;https://www.bilibili.com/video/BV1GJ411x7h7&lt;/code&gt;，一样可以正常播放。&lt;/p&gt;
&lt;p&gt;感兴趣的话还可以猜测一下链接后面的一大堆参数的具体作用。简单猜测和分析可知，&lt;code&gt;mid=114514&lt;/code&gt;是分享这个链接的用户的b站uid，&lt;code&gt;share_medium=android&lt;/code&gt;表示这个用户是通过安卓平台分享的链接，&lt;code&gt;share_source=COPY&lt;/code&gt;表示分享方式是复制链接，&lt;code&gt;timestamp=1658778647&lt;/code&gt;表示分享这个链接的时间戳，用一些在线时间戳转换网站转换可知这个链接被分享的时间是&lt;code&gt;2022-07-26 03:50:47&lt;/code&gt;。因此，删除这些参数除了可以大幅缩短链接长度外，也能防止部分个人信息被泄露出去。&lt;/p&gt;
&lt;h2&gt;其他类型的URL&lt;/h2&gt;
&lt;p&gt;其他的URL和http(s)也差不多，都是这种格式，比如steam一键入库APEX的链接是
&lt;code&gt;steam://install/1172470&lt;/code&gt;
简单分析可知，协议是&lt;code&gt;steam&lt;/code&gt;，访问的路径是&lt;code&gt;install/1172470&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;邮件发送链接是
&lt;code&gt;mailto:xxx@xxx.com&lt;/code&gt;
协议是&lt;code&gt;mailto&lt;/code&gt;（部分URL是没有//的），访问的路径（目标邮箱）是&lt;code&gt;xxx@xxx.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;参考文献：&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc1738.html&quot;&gt;rfc1738&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>哈希算法是什么，它有什么作用</title><link>https://lq0.tech/posts/hash/</link><guid isPermaLink="true">https://lq0.tech/posts/hash/</guid><description>用通俗易懂的方式介绍哈希算法的原理和用途</description><pubDate>Sat, 23 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文的目的是为了让没什么计算机基础的人也能看懂哈希算法的作用，如果觉得有看不懂或写得不对/不好的地方欢迎联系本人，我会修改到尽量让0基础的人也能看懂&lt;/p&gt;
&lt;h1&gt;什么是哈希算法&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Hash算法&lt;/strong&gt; 有很多的译名，一般被翻译成哈希、散列、杂凑等（本文就称其为哈希），有时也被叫做信息摘要啥的，本意是指&quot;把...弄糟，搞砸&quot;
它的作用也和它的名字类似，给定一个&lt;strong&gt;任意长度&lt;/strong&gt;输入，它能根据输入计算出一个&lt;strong&gt;长度确定&lt;/strong&gt;的值&lt;/p&gt;
&lt;p&gt;以一种哈希算法md5举例，如果你输入的内容是&lt;code&gt;114514&lt;/code&gt;，那得到的结果（哈希值）就是&lt;code&gt;c4d038b4bed09fdb1471ef51ec3a32cd&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果只是这样的话那这个算法也没啥用，但哈希算法还有几个特性：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于所有计算出来的哈希值，只要两个哈希值不同，那原输入就一定不同&lt;/li&gt;
&lt;li&gt;哈希算法具有&lt;a href=&quot;https://baike.baidu.com/item/%E9%9B%AA%E5%B4%A9%E6%95%88%E5%BA%94/5988567&quot;&gt;雪崩效应&lt;/a&gt;，原输入哪怕只改变一点点，那计算出的哈希值都会完全不同&lt;/li&gt;
&lt;li&gt;无法直接从算出来的哈希值反推出原先的输入（因为计算的时候会丢失很多信息）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;举个例子，以下是几个不同的数的md5值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;114514  =&amp;gt; c4d038b4bed09fdb1471ef51ec3a32cd
114513  =&amp;gt; 77666beaaa7fd86de9b9373ded05db44
11451   =&amp;gt; f0eb6568ea114ba6e293f903c34d7488
1145141 =&amp;gt; 9e39382c84a52a4b8fe2e594955901c8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现没有？不管你是改一位数、增加一位数还是删除一位数，最后得到的md5值都&lt;strong&gt;完全不同&lt;/strong&gt;
如果不相信的话可以找个在线计算md5（别的哈希算法也行）的&lt;a href=&quot;https://ctfever.uniiem.com/tools/message-digest&quot;&gt;网站&lt;/a&gt;试试&lt;/p&gt;
&lt;p&gt;正是因为这些特性，哈希算法才能被用于各种用途&lt;/p&gt;
&lt;h1&gt;哈希算法的各种用途&lt;/h1&gt;
&lt;p&gt;由于以上的特性，哈希算法可以用来验证原数据是否是某段数据，就像你识别一个人可以观察TA的指纹，而不需要再观测别的信息。因此，哈希算法有着各种各样的用途&lt;/p&gt;
&lt;h2&gt;文件完整性校验&lt;/h2&gt;
&lt;p&gt;上文有提到哈希算法对于输入的数值非常敏感，哪怕你只改了一个比特，那得到的结果都会完全不同&lt;/p&gt;
&lt;p&gt;因此，假设我们有个网站提供文件A的下载，只要我们把文件A的哈希值计算出来并放在下载按钮旁边，那用户在拿到文件后只需要在本地计算一遍文件的哈希值再和网站上放的哈希值对比，就可以知道下载下来的问题是否有被损坏&lt;/p&gt;
&lt;p&gt;注：哈希算法&lt;strong&gt;只能保证文件未被修改，不能保证提供者是可信的&lt;/strong&gt;。如果真想伪造网站的话可以连文件的哈希值也一起伪造&lt;/p&gt;
&lt;p&gt;这种校验也被各种程序使用，假如你有个程序能自动下载并安装某模块，为了防止下载下来的模块有问题导致运行出错，可以让服务器提供一下这个模块的哈希值。然后程序就能自动计算下载下来的模块的哈希值，并和服务器提供的哈希值进行比较，如果不一致就表明下载下来的文件有问题。这时候程序就能选择重新下载这个模块，而不是直接用这个模块然后运行时疯狂报错，还打死都找不到是哪出错了&lt;/p&gt;
&lt;p&gt;一些网盘之类的文件扫描、文件秒传也是用哈希算法实现的。秒传的原理就是在上传文件前，客户端先把文件的哈希值计算出来并告诉服务器，服务器就去查找是否有存储过对应哈希值的文件，有的话就直接告诉客户端这文件已经存储过了，然后客户端就直接告诉用户文件上传完成，没有的话再去真正地上传文件。违规文件扫描也是这样的，服务器存储违规文件的哈希值，这样客户端上传文件的时候只要文件的哈希值被识别为违规，那上传就会直接被拒绝。这也是为什么一些聊天软件的图片屏蔽也只要改原图的一个像素都可以轻易绕过，因为哈希算法对输入非常敏感，改一个字节算出的哈希值都完全不同。只用哈希算法进行的文件校验都可以通过修改文件内容（哪怕只改一个字）的方法直接绕过&lt;/p&gt;
&lt;h2&gt;安全存储用户的密码&lt;/h2&gt;
&lt;p&gt;假设有个网站要存用户的用户名和密码到数据库里，那可以先看看以下几个方案&lt;/p&gt;
&lt;h3&gt;明文存储&lt;/h3&gt;
&lt;p&gt;这是最容易想到的思路，但同时这也是&lt;strong&gt;最不安全&lt;/strong&gt;的存储方式，将用户输入的密码原样存进数据库里。为什么这么做不安全？万一网站后台有内鬼，或者因为别的什么原因数据库泄露了，那就直接寄了，别人可以直接用数据库里存的密码登录你网站的任何用户&lt;/p&gt;
&lt;p&gt;最关键的是这样不仅会导致你网站的数据全都寄了，而且还会危害使用你网站的所有用户。很多用户喜欢在所有网站都用同一套（或类似的）用户名和密码，你泄露了用户在A网站注册用的用户名和密码，那攻击者拿到后可以拿它们去B、C、D、E、F网站尝试，就算这么做的成功率不是很高，但只要尝试的数量够多，总会有人中招的，因此作为用户也千万别全网都用同一套用户名和密码&lt;/p&gt;
&lt;h3&gt;对密码进行对称加密后存储&lt;/h3&gt;
&lt;p&gt;这样做虽说会比明文存储安全（废话），但也是有挺高的风险的&lt;/p&gt;
&lt;p&gt;对称加密基本就是指用一个密码加密数据后，你也只能用这个密码来解密数据。但别忘了一件事，进行对称加密所需的密码你肯定也是要找个地方存储的，况且每次验证用户登录你也必须要使用这个密码，因此当数据库泄露后，如果这个密码也被一起泄露了，那一样是白给&lt;/p&gt;
&lt;h3&gt;存储密码的哈希值&lt;/h3&gt;
&lt;p&gt;还记得上文所说的哈希算法的特性吗？哈希算法是&lt;strong&gt;不可逆&lt;/strong&gt;的，当你得到一个哈希值的时候，你是没法反推出它的原始值的。这样看上去用密码的哈希值来验证密码就刚好了。我们不存储原始的密码，而是直接存储密码的哈希值。这样验证密码的时候我们也不拿原始密码去对比，而是先计算用户输入的密码的哈希值，再去数据库里对比存储的密码哈希值&lt;/p&gt;
&lt;p&gt;这样，我们在不知道原始密码的情况下依然可以验证用户输入的密码，同时由于哈希算法的特性，就算有人得到了数据库里存储的哈希值，他也没法通过哈希值逆推出原先的密码。所以为什么现在的app和网站在你忘记密码的时候都没法告诉你原始密码，只能让你重新设置？因为服务器自己都不知道你原始密码到底是什么，只有密码的哈希值，因此只能让你去重新设置了&lt;/p&gt;
&lt;p&gt;这样做既保证了登录验证功能能正常实现，同时也能保证就算数据泄露用户的密码也不会跟着被泄露，看上去这是个完美的解决方案。&lt;/p&gt;
&lt;p&gt;是吗？&lt;/p&gt;
&lt;h3&gt;存储密码的加盐哈希值&lt;/h3&gt;
&lt;p&gt;虽然哈希算法是不可逆的，但有个攻击方法叫彩虹表&lt;/p&gt;
&lt;p&gt;我虽然知道&lt;code&gt;114514&lt;/code&gt;的md5值是&lt;code&gt;c4d038b4bed09fdb1471ef51ec3a32cd&lt;/code&gt;，但没法用&lt;code&gt;c4d038b4bed09fdb1471ef51ec3a32cd&lt;/code&gt;反推出&lt;code&gt;114514&lt;/code&gt;，那怎么办？简单，那就自己搞个数据库，在里面存入数据&lt;code&gt;c4d038b4bed09fdb1471ef51ec3a32cd =&amp;gt; 114514&lt;/code&gt;。既然没法逆推出原始数据，那就生成一堆常见的数据，并把它们的哈希值算出来，再存进我自己这个数据库里，这样下次遇到对应的哈希值就去查自己的数据库就好了。当然，你不可能穷举所有的排列组合并把它们的哈希值都存储起来，因此这个方法也只能破解常见密码的哈希值&lt;/p&gt;
&lt;p&gt;那你总不能说我数据库泄露后只能保证密码复杂的人的密码才不会破解吧 &lt;s&gt;（当然你要觉得用简单密码的人都是傻逼，活该密码被破解的话当我没说）&lt;/s&gt; ，因此我们可以使用加盐哈希来存储密码&lt;/p&gt;
&lt;p&gt;什么是加盐哈希？简单来说就是我们在计算哈希值之前，先对原先的数据进行处理，加上一点&quot;盐&quot;，再去计算加盐后的数据的哈希值&lt;/p&gt;
&lt;p&gt;举个例子，&lt;code&gt;p@ssword&lt;/code&gt;是个常见密码，它的md5值是&lt;code&gt;90f2c9c53f66540e67349e0ab83d8cd0&lt;/code&gt;，你可以在&lt;a href=&quot;https://www.cmd5.com/&quot;&gt;一些md5破解网站&lt;/a&gt;（这些网站的原理就是刚刚说的彩虹表）试试看这段md5值，都能直接查询出来&lt;/p&gt;
&lt;p&gt;但如果我给&lt;code&gt;p@ssword&lt;/code&gt;后面加上&lt;code&gt;TURaWg==&lt;/code&gt;再计算md5值呢？那&lt;code&gt;p@sswordTURaWg==&lt;/code&gt;的md5值就是&lt;code&gt;17e7c834da0551e8872d77f741b58bbc&lt;/code&gt;，和刚刚的值完全不同（别忘了上文说的哈希算法具有&lt;strong&gt;雪崩效应&lt;/strong&gt;，差一个字结果都完全不同），你再去这些破解网站就查不出来了。这样就算泄露了密码哈希值和盐的值，攻击者也必须得重新跑一遍彩虹表才有可能得知用户的密码是否是某些常见密码，要付出极高的时间成本。但假如我给每个用户都加上随机的的盐呢？那攻击者就真要疯了，要是还想暴力破解的话得针对每个用户都去跑个彩虹表，时间成本高到根本无法接受（当然这样依然是有可能跑出简单的密码的，所以&lt;strong&gt;别使用常见或简单的密码&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;所以，存储用户密码的时候可以这么设计，往数据库里存&lt;code&gt;用户id + 密码的加盐哈希值 + 随机生成的盐&lt;/code&gt;，验证用户登录的时候就先读取盐，再把用户输入的密码加上盐之后计算哈希值，最后再和数据库里的加盐哈希值进行比较，这样可以有效避免密码被暴力破解。为了尽量防止密码被暴力破解，还可以换用计算时间所需更久的哈希算法&lt;/p&gt;
&lt;p&gt;思考题：盐的内容对暴力破解的难度有影响吗？盐的长度呢？如果我不在密码后面加盐，而是在密码的前面加盐呢？如果我再换别的加盐方式，比如每隔一个字把盐的一个字符穿插在密码中间呢？&lt;/p&gt;
&lt;h3&gt;数字签名技术&lt;/h3&gt;
&lt;p&gt;前面说过哈希算法可以用来整文件完整性校验，但不能确保文件的提供者是可信的。不过配合上非对称加密就可以做到这一点，这种技术被称作数字签名。有关数字签名的内容我不打算在这篇文章介绍，涉及的内容比较多，感兴趣的话可以看看其他人的讲解&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1Eo4y1y7Dh/&quot;&gt;【软件科普】如何保证发出去的微信和QQ消息不被篡改？详解RSA加密算法&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;哈希算法的漏洞&lt;/h2&gt;
&lt;p&gt;哈希算法也并不是无敌的，也存在着一些漏洞&lt;/p&gt;
&lt;h3&gt;哈希碰撞&lt;/h3&gt;
&lt;p&gt;正如你可以通过假指纹的方式骗过指纹验证，哈希算法也有办法碰撞（即用不同的输入得到相同的哈希值）&lt;/p&gt;
&lt;p&gt;以md5为例，它输出的结果是个32位的16进制数，那就一共有 16^32^ = 340282366920938463463374607431768211456 种可能的排列组合。别的哈希算法的组合可能更多，但本质还是一样的。虽然这个数非常大，但跟哈希算法可能遇到的各种输入，这个数是远远不够的。计算哈希值的时候都会丢失很多的信息。由于鸽笼原理可知，当我们把10个鸽子存进5个笼子的时候，必定有至少一个笼子里有一只以上的鸽子。因此，当数据的排列组合够多的时候，必然也会产生哈希碰撞，即两个不一样的数据能计算出相同的哈希值。举个最简单的例子，md5的输出是32位的16进制数，那你给他输入100位的16进制数的所有排列组合，它一定会生成很多一样的哈希值&lt;/p&gt;
&lt;p&gt;其实可以反过来想，如果哈希值不会碰撞，那就表明一个哈希值能对应一个唯一的原数据，那岂不是说我可以直接用个md5值来代替一个1G的电影，然后存储的时候直接存储那个md5，要播放的时候直接根据这个md5计算出电影的原始数据然后直接播放，这样的话你还买硬盘干什么（&lt;/p&gt;
&lt;p&gt;哈希碰撞并不是危言耸听，像md5就被爆出了能快速构造出两个md5相同的不同文件的漏洞&lt;/p&gt;
&lt;p&gt;结合上文提到的哈希算法的用途，那就有以下的攻击手段&lt;/p&gt;
&lt;p&gt;比如文件下载的时候，正常的文件A哈希值是a，但如果攻击者能想办法构造出一个病毒文件B，并让它的哈希值也为a，那就可以欺骗过简单的文件校验，让别人或别的程序以为A和B是同一个文件。因此，在检验哈希值的时候还可以顺便校验下文件的大小，这样可以大幅降低被假的文件欺骗的概率&lt;/p&gt;
&lt;p&gt;不过虽然存在哈希碰撞这种攻击手段，但这并不是说哈希算法就一无是处，就能随便生成相同的哈希值了。虽然理论上是一定可以生成哈希值相同的数据的，但实际上要找到这样的数据是极其困难的，哪怕是已经被证明有缺陷的md5也只能生成两个哈希值相同的文件。对于一个给定的md5值，还是没法逆推出原数据或者可以和原数据哈希碰撞的值。而且现在有不少算法至今仍未被找出碰撞案例，例如sha-256、sha-384和sha-512算法。即使是已被证明为不安全的哈希算法，我们仍然可以在对安全性没什么要求的地方继续使用它们进行简单的验证。但在安全性要求高的地方，还是得用安全的哈希算法来进行各种操作&lt;/p&gt;
</content:encoded></item><item><title>Light_Quanta[2] Bot 使用帮助</title><link>https://lq0.tech/posts/bot-help/</link><guid isPermaLink="true">https://lq0.tech/posts/bot-help/</guid><description>Light_Quanta[2] Bot 使用帮助</description><pubDate>Wed, 22 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;Light_Quanta[2] Bot 的所有功能均由 Light_Quanta 本人开发（QQ: 2224899528）&lt;/p&gt;
&lt;p&gt;有建议的话欢迎直接跟我提
我会尽快回复&lt;/p&gt;
&lt;p&gt;&lt;s&gt;尽管我感觉很多功能都搞得乱七八糟的&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;:::note
bot的大部分功能都可以私聊使用，请不要在群里刷屏，私聊使用还可以无视大多数功能的冷却时间
:::&lt;/p&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2025-03-18&lt;/p&gt;
&lt;p&gt;目前bot暂停自动接收邀请加群请求，如有需求请联系本人
:::&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本Bot&lt;strong&gt;开源&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;源码可以在&lt;a href=&quot;https://github.com/LightQuanta/LQ1Bot&quot;&gt;这里&lt;/a&gt;查看&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;新版Bot源码可以在&lt;a href=&quot;https://github.com/LightQuanta/LQ2Bot&quot;&gt;这里&lt;/a&gt;查看&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;关于好友请求和邀请入群&lt;/p&gt;
&lt;p&gt;bot会&lt;strong&gt;自动同意所有好友请求以及邀请入群请求&lt;/strong&gt;，如果需要加bot好友/邀请bot入群无需通知Light_Quanta&lt;/p&gt;
&lt;p&gt;若出现加群失败或无视邀请加群请求等现象，请联系Light_Quanta进行处理&lt;/p&gt;
&lt;p&gt;bot的绝大多数功能都可以开关，详见下文&lt;/p&gt;
&lt;p&gt;若不需要bot可以直接把bot移出群聊，无需联系Light_Quanta，但如果是因为一部分功能过于刷屏，或是只想使用一部分功能的话，烦请参见下文介绍来单独控制各个功能的开关&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;:::warning[关于指令的示例（重要）]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;(xxx)&lt;/code&gt; 表示中括号里的内容是&lt;em&gt;&lt;strong&gt;必填&lt;/strong&gt;&lt;/em&gt; 的
&lt;code&gt;[xxx]&lt;/code&gt; 表示中括号里的内容是&lt;em&gt;&lt;strong&gt;可选&lt;/strong&gt;&lt;/em&gt; 的&lt;/p&gt;
&lt;p&gt;输入指令时请&lt;/p&gt;
&lt;p&gt;&amp;lt;p style=&quot;font-size:60px; font-weight:bolder; margin:-30px 0;&quot;&amp;gt;不要输入括号&amp;lt;/p&amp;gt;
&amp;lt;p style=&quot;font-size:60px; font-weight:bolder; margin:-30px 0;&quot;&amp;gt;不要输入括号&amp;lt;/p&amp;gt;
&amp;lt;p style=&quot;font-size:60px; font-weight:bolder; margin:-30px 0;&quot;&amp;gt;不要输入括号&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;例如对于指令&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;dice[ adb]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;既可以使用 &lt;code&gt;dice&lt;/code&gt; 触发，也可以使用 &lt;code&gt;dice 114d514&lt;/code&gt; 触发&lt;/p&gt;
&lt;p&gt;对于指令&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;!subscribe (主播的b站uid)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以使用 &lt;code&gt;!subscribe 1463028352&lt;/code&gt; 触发&lt;/p&gt;
&lt;p&gt;对于指令&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;where (osu!用户名) [游戏模式]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以使用 &lt;code&gt;where Light_Quanta&lt;/code&gt; 或 &lt;code&gt;where Light_Quanta std&lt;/code&gt; 触发
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;主要功能列表&lt;/h2&gt;
&lt;h3&gt;启用/禁用bot&lt;/h3&gt;
&lt;p&gt;以下指令只有管理员、群主以及bot管理员可以使用&lt;/p&gt;
&lt;p&gt;输入&lt;code&gt;!enablebot&lt;/code&gt;可以在本群启用bot&lt;/p&gt;
&lt;p&gt;输入&lt;code&gt;!disablebot&lt;/code&gt;可以在本群禁用bot&lt;/p&gt;
&lt;h3&gt;功能控制&lt;/h3&gt;
&lt;p&gt;输入&lt;code&gt;!plugins&lt;/code&gt;可以查看当前群的Bot各个插件的启用状态&lt;/p&gt;
&lt;p&gt;大部分功能是默认启用的&lt;/p&gt;
&lt;p&gt;以下指令只有管理员、群主以及bot管理员可以使用&lt;/p&gt;
&lt;p&gt;使用以下指令关闭一个功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!disable (功能名称)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用以下指令开启一个功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!enable (功能名称)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用以下指令重置一个功能的启用状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!reset (功能名称)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，禁用今天看谁的话，输入以下指令即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!disable DDTool
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;智能回复&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;功能名称：IntelligentReply&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在&lt;strong&gt;群&lt;/strong&gt;里@bot的账号发送功能调用请求，bot会尝试理解并直接调用指定功能&lt;/p&gt;
&lt;p&gt;注：目前仅为部分功能接入了智能回复，若无法用该方式调用请继续参考文档进行手动设置&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Light_Quanta[2] 发点哈基米图
@Light_Quanta[2] 这个群订阅了哪些主播
@Light_Quanta[2] 现在谁在直播
@Light_Quanta[2] 抽个禁言套餐
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Meme&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;功能名称：Meme&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;根据发送消息自动随机回复预设内容（不区分大小写）&lt;/p&gt;
&lt;p&gt;为避免刷屏，超过500字的回复内容会在发送一分钟后自动撤回&lt;/p&gt;
&lt;p&gt;支持全文匹配、正则表达式匹配、正则表达式替换和文本开头匹配&lt;/p&gt;
&lt;p&gt;Meme内容&lt;strong&gt;只能&lt;/strong&gt;由Bot管理员进行审核后更新，如果你想提供&lt;strong&gt;建议&lt;/strong&gt;，可以使用以下这些指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(操作类型)(关键词检测类型) (触发关键词或id)#回复内容1[|回复内容2|回复内容3|...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中操作类型可以是&lt;code&gt;add&lt;/code&gt;和&lt;code&gt;addgroup&lt;/code&gt;，分别表示添加全局可用的回复和添加只在当前群可用的回复&lt;/p&gt;
&lt;p&gt;关键词检测类型可以是&lt;code&gt;meme&lt;/code&gt;、&lt;code&gt;regexmeme&lt;/code&gt;、&lt;code&gt;startswithmeme&lt;/code&gt;、&lt;code&gt;regexreplacememe&lt;/code&gt;，分别表示全文匹配、正则表达式匹配、开头匹配和正则表达式替换（注：正则表达式替换需要全文匹配）&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;addmeme 114#514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会添加一个全局可用的回复，对于&lt;code&gt;114&lt;/code&gt;会回复&lt;code&gt;514&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;addgroupmeme 114#514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会添加一个仅在当前群可用的Meme，对于当前群里发送的&lt;code&gt;114&lt;/code&gt;会回复&lt;code&gt;514&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;addregexmeme \d+#number!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会添加一个全局可用的回复，对于符合正则表达式&lt;code&gt;\d+&lt;/code&gt;的内容，回复&lt;code&gt;number!&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;删除对于指定内容的全部回复&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delmeme (触发关键词或id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若想删除指定关键词的部分回复，可以用和&lt;code&gt;addmeme&lt;/code&gt;一样的格式发送&lt;code&gt;delmeme&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delmeme (触发关键词或id)#回复内容1[|回复内容2|回复内容3|...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;群管理或群主如果想在群里禁用某些回复，可以使用以下指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;banmeme (触发关键词或id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;banmeme 来点二次元
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要解封的话，将&lt;code&gt;banmeme&lt;/code&gt;换成&lt;code&gt;unbanmeme&lt;/code&gt;即可&lt;/p&gt;
&lt;p&gt;如果想为关键词添加别名，请使用以下指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;addalias (原触发关键词或id)#(新触发关键词1)[|新触发关键词2|新触发关键词3...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，原本的触发关键词为&lt;code&gt;A&lt;/code&gt;，现在想添加两个新触发关键词&lt;code&gt;B&lt;/code&gt;和&lt;code&gt;C&lt;/code&gt;，使得无论是发送&lt;code&gt;A&lt;/code&gt;、&lt;code&gt;B&lt;/code&gt;还是&lt;code&gt;C&lt;/code&gt;都能触发，可以使用以下指令添加别名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;addalias A#B|C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若要修改Meme的其他信息（关键词、检测类型、群黑/白名单等），请使用以下指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;modifymeme (触发关键词或id) (操作类型) (参数)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（建议触发关键词填写id，避免识别错误）&lt;/p&gt;
&lt;p&gt;操作类型可选项有:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;addwhitelist (添加群白名单)
removewhitelist (移除群白名单)
addblacklist (添加群黑名单)
removeblacklist (移除群黑名单)
rename (重命名Meme)
modifytype (修改检测类型)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检测类型的可选项为&lt;code&gt;equal&lt;/code&gt;、&lt;code&gt;regex&lt;/code&gt;、&lt;code&gt;regexreplace&lt;/code&gt;、&lt;code&gt;startswith&lt;/code&gt;，作用请参考前文&lt;/p&gt;
&lt;p&gt;如果想查看一个Meme的详细信息（id，触发类型，触发关键词，别名，回复内容等），可以使用以下指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;getmemejson (触发关键词或id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要查找是否存在某一关键词，可以使用以下指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;findmeme (部分关键词或id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想查找某个关键词是否有对应的回复内容，可以使用以下指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;findmeme (完整关键词或id)#(部分回复内容)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如需要指定回复内容，可以&lt;strong&gt;私聊&lt;/strong&gt;bot使用以下指令指定&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setrep (完整关键词或id)#(部分回复内容)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;恶臭检测&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;此功能属于无情的接梗机器&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;当发言满足正则表达式&lt;code&gt;哼哼哼?[,，]? ?啊啊啊+&lt;/code&gt;时，自动回复臭死了&lt;/p&gt;
&lt;h4&gt;定型文&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;此功能属于无情的接梗机器&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;自动将指定的名称带入各种定型文&lt;/p&gt;
&lt;p&gt;以下内容不一定为最新&lt;/p&gt;
&lt;p&gt;用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;定型文 (名称)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入后会随机发送一篇定型文&lt;/p&gt;
&lt;p&gt;如果使用此格式输入可以自定义发送哪篇定型文&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;定型文n (名称)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中n为定型文id&lt;/p&gt;
&lt;p&gt;目前的定型文有这些（按照id排序）&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;我忘不掉{str}了。\n如果不是知道了{str}......&lt;/li&gt;
&lt;li&gt;我真的想{str}想得要发疯了。我躺在床上会想{str}......&lt;/li&gt;
&lt;li&gt;{str}瘾发作最严重的一次，躺在床上，难受的一直刷{str}的录播......&lt;/li&gt;
&lt;li&gt;言いたいことがあるんだよ！\nやっぱり{str}はかわいいよ！......&lt;/li&gt;
&lt;li&gt;{str}每早七点按时起床叠被子刷牙晨读十分钟诗歌选集......&lt;/li&gt;
&lt;li&gt;你知道雪为什么是白色的吗？那是因为它忘记了自己的颜色......&lt;/li&gt;
&lt;li&gt;早上为什么睁开眼睛，因为有{str}的起床闹钟在叫我起床......&lt;/li&gt;
&lt;li&gt;{str}给我的感觉，就是一切都很甜。她收起了一些感情......&lt;/li&gt;
&lt;li&gt;{str}就是白砂糖，香辛料和某种美好的东西组成的女孩子......&lt;/li&gt;
&lt;li&gt;最讨厌{str}了\n总是多管我的闲事，人也笨，麻烦......&lt;/li&gt;
&lt;li&gt;我好想做{str}的狗啊\n可是{str}说她喜欢的是猫......&lt;/li&gt;
&lt;li&gt;{str}卡哇伊哈啊suki{str}我一直都很喜欢{str}......&lt;/li&gt;
&lt;li&gt;我忘不了{str}了。那天在咖啡厅里，我们相互交换了联系方式......&lt;/li&gt;
&lt;li&gt;每晚都因为思念{str}而难以入眠。\n{str}娇嫩的面孔是那样可爱......&lt;/li&gt;
&lt;li&gt;在最美的夜里想{str}，在最深的呼吸中念{str}......&lt;/li&gt;
&lt;li&gt;在我眼中{str}是最美的，她拥有所有女孩梦寐以求的东西......&lt;/li&gt;
&lt;li&gt;想和{str}去100个城市来99个拥抱看个拥抱看98场日落......&lt;/li&gt;
&lt;li&gt;今天打假，我打电话举报说${str}喜欢我，结果人家说我有病......&lt;/li&gt;
&lt;li&gt;${str}依然这么可爱。\n什么都没变，是我没跟上她们......&lt;/li&gt;
&lt;li&gt;我的手总是忍不住点进billbill。\n总是忍不住在关注动态里寻找着什么......&lt;/li&gt;
&lt;li&gt;每当打开${str}的评论区，我就一阵惶恐......&lt;/li&gt;
&lt;li&gt;我是${str}周边的一粒微尘，不知何时起......&lt;/li&gt;
&lt;li&gt;${str}晚安的一吻，吻在了我的灵魂......&lt;/li&gt;
&lt;li&gt;好想和${str}相遇在3月，用她的微笑驱散余寒......&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;除此之外还有些定型文可以用以下几个指令触发&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(名称1)好想做(名称2)的狗啊
(名称1)忘不掉(名称2)了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为防止刷屏，超过200字的定型文会在发送后60秒自动撤回&lt;/p&gt;
&lt;h4&gt;营销号生成器&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;此功能属于无情的接梗机器&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;自动生成营销号文章&lt;/p&gt;
&lt;p&gt;用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;营销号 (名称)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;苏联笑话生成器&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;此功能属于无情的接梗机器&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;自动生成苏联笑话&lt;/p&gt;
&lt;p&gt;用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;苏联笑话 (名称)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;禁言抽奖&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;功能名称： Mute&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;顾名思义，发送&lt;code&gt;禁言抽奖&lt;/code&gt;即可抽到时长随机的禁言&lt;/p&gt;
&lt;p&gt;具体的禁言时间为1~180分钟，可以&lt;a href=&quot;https://github.com/LightQuanta/LQ2Bot/blob/main/src/main/kotlin/tech/lq0/plugins/Mute.kt#L16&quot;&gt;参考源代码&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;自助禁言&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;功能名称： Mute&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;顾名思义，可以自助禁言&lt;/p&gt;
&lt;p&gt;使用方法（单位：分钟）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;自助禁言 (时长)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;自助禁言 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;dice骰子&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;功能名称： Dice&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;顾名思义，是一个可以自定义面数的骰子，还可以最多同时投掷10个骰子&lt;/p&gt;
&lt;p&gt;用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dice[ adb]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中a为一次性投掷的骰子数量，b为面数&lt;/p&gt;
&lt;p&gt;如果直接输入dice则默认为1d6&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dice 10d1000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将会投出10个1000面的骰子&lt;/p&gt;
&lt;h3&gt;今天看谁/现在看谁&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;功能名称： DDTool&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;从&lt;a href=&quot;https://vtbs.moe/&quot;&gt;vtbs.moe&lt;/a&gt;记录的管人列表里随机抽取一个管人并展示其简介等基本信息&lt;/p&gt;
&lt;p&gt;使用以下指令可以随机抽取一个管人&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;今天看谁
今天D谁
现在看谁
现在D谁
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为防止刷屏，在使用该功能时，每个人将会有五分钟的冷却时间，冷却时间内发送无效&lt;/p&gt;
&lt;p&gt;如果是群管或群主的话，可以设置此功能只能抽到指定的用户，输入以下指令即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!setddtool (目标用户uid)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;s&gt;什么强制单推&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;输入&lt;code&gt;!setddtool reset&lt;/code&gt;可以解除绑定&lt;/p&gt;
&lt;p&gt;数据来源：&lt;a href=&quot;https://vtbs.moe&quot;&gt;vtbs.moe&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;恶臭论证&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;功能名称： HomoCalc&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;发送以下指令会生成数字论证&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;恶臭论证 (要论证的数字)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;s&gt;然而这弱智算法生成的算式又臭又长&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;之后我再想办法优化&lt;/s&gt;&lt;/p&gt;
&lt;h3&gt;来点猫图&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;功能名称： CatPic&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;随机获取一张猫图&lt;/p&gt;
&lt;p&gt;发送以下内容即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;来点猫猫
来点猫图
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数据来源：&lt;a href=&quot;https://thecatapi.com/&quot;&gt;TheCatApi&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;警钟&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;功能名称：Alarm&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;精确到100纳秒的警钟&lt;/p&gt;
&lt;p&gt;在群里发送以下指令即可使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;警钟
警钟长鸣
警钟敲烂
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个群可以绑定多个警钟，以上3个指令只会抽取最新的警钟&lt;/p&gt;
&lt;p&gt;要抽取指定警钟，请使用以下指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;警钟(警钟id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以使用以下指令一次性发送所有警钟的内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;编钟
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用以下指令可以获得所有警钟的内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!getalarm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;管理员和群主以及bot管理可以使用以下指令添加和删除警钟&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!addalarm (初始时间)|(回复内容)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，初始时间的格式应为&lt;code&gt;年-月-日 时:分:秒&lt;/code&gt;，例如&lt;code&gt;2022-6-24 21:19:07&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;回复内容里使用&lt;code&gt;{time}&lt;/code&gt;可以自动计算当前时间和初始时间的差值，精确到100纳秒&lt;/p&gt;
&lt;p&gt;输入以下指令删除&lt;strong&gt;所有&lt;/strong&gt;的警钟&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!delalarm
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;开播通知&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;功能名称： LiveNotify&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在主播开播/下播时自动通知&lt;/p&gt;
&lt;p&gt;此功能仅限于b站主播的开播通知，每30s检测一次&lt;/p&gt;
&lt;p&gt;使用以下指令查看本群订阅的主播列表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!showsubscribe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用以下指令查看当前正在直播的主播列表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!live
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;管理员、群主以及bot管理员可以用以下指令订阅主播（注意id是主播的b站uid，&lt;strong&gt;不是直播间号&lt;/strong&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!subscribe (主播的b站uid)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用以下指令取消订阅&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!unsubscribe (主播的b站uid)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个群可以订阅最多300个主播&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!subscribe 1463028352
!subscribe https://space.bilibili.com/1463028352
!subscribe 1463028352(理塘丁真)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以一次性订阅或取消订阅多个主播，示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!subscribe 1463028352 402574397 163637592
!subscribe 1463028352，402574397,163637592
!unsubscribe 1463028352, 402574397 163637592
!unsubscribe https://space.bilibili.com/1463028352, 402574397(孙笑川258)   UID: 163637592
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;管理员、群主以及bot管理员可以使用以下指令取消订阅当前群订阅的所有主播&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!clearsubscribe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直播推送可以更改部分播报相关配置，群成员可以使用&lt;code&gt;!showliveconfig&lt;/code&gt;查看群内相关配置&lt;/p&gt;
&lt;p&gt;群主和管理员可以使用&lt;code&gt;!liveconfig&lt;/code&gt;指令调整推送相关设置，格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!liveconfig (配置项名称) (操作)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中配置项名称为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下播通知&lt;/li&gt;
&lt;li&gt;显示直播时长&lt;/li&gt;
&lt;li&gt;显示直播间标题&lt;/li&gt;
&lt;li&gt;显示直播间封面&lt;/li&gt;
&lt;li&gt;显示直播间链接&lt;/li&gt;
&lt;li&gt;显示直播分区&lt;/li&gt;
&lt;li&gt;直播时通知直播间标题更改&lt;/li&gt;
&lt;li&gt;非直播时通知直播间标题更改&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：显示直播时长需要打开下播通知才可以看到&lt;/p&gt;
&lt;p&gt;操作可以是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启用&lt;/li&gt;
&lt;li&gt;禁用&lt;/li&gt;
&lt;li&gt;开&lt;/li&gt;
&lt;li&gt;关&lt;/li&gt;
&lt;li&gt;true&lt;/li&gt;
&lt;li&gt;false&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;等，挑选任意一个有效的即可&lt;/p&gt;
&lt;p&gt;以上配置除了&lt;code&gt;非直播时通知直播间标题更改&lt;/code&gt;外全都默认启用&lt;/p&gt;
&lt;p&gt;注：在把bot移出群聊后，该群聊的订阅信息也会被清空&lt;/p&gt;
&lt;p&gt;使用以下指令查看开播通知功能的统计数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!livestatis
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>