├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc ├── Installation-for-dummies.md └── intro.md ├── examples ├── config │ └── default.edn └── scripts │ └── myscript.act ├── project.clj ├── src └── actionne │ ├── classpath.clj │ └── core.clj └── test └── actionne └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2019-02-16 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2019-02-16 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/your-name/actionne/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/actionne/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # actionne 2 | 3 | 一个基于规则的动作触发器框架 4 | 5 | 6 | ## Features 7 | 8 | * 用简单的DSL定义条件和定义动作 9 | * 用plugin定义输入数据集和动作函数 10 | * 自定义配置项 11 | 12 | ## Usage 13 | 14 | Check out this [dummy guide](doc/Installation-for-dummies.md) if you're new to the framework :) 15 | 16 | actionne 的目的是实现一个基于规则运行某个函数的框架。它可以解决很多问题,从删除符合一组自定义规则的信息,到控制智能家居设备,都可以基于它实现。 17 | 18 | 为了足够的灵活性和可扩充性,actionne使用了插件和DSL设计。用户可以使用DSL语言描述业务规则,当获得了符合规则的消息,actionne会直接调用插件中的函数,从而完成用户预期的动作。 19 | 20 | actionne requires Clojure 1.9.0 or later. 21 | 22 | 23 | Run actionne: 24 | 25 | java -Dhomedir="/YOUR_PATH" -jar actionne-0.1.0-SNAPSHOT-standalone.jar 26 | 27 | 28 | ## Set up actionne 29 | 30 | 如果是第一次运行,actionne会创建 /YOUR_PATH 目录,并因为无法找到配置文件退出。 31 | 32 | 目录结构如下: 33 | 34 | /YOUR_PATH 35 | /config 配置文件 36 | /data 用户数据 37 | /plugins 插件 38 | /scripts DSL脚本 39 | 40 | 可以参考examples目录的例子。 41 | 42 | ### 插件 43 | 44 | * 目前只有 twitter 插件: [twitter_actionne](https://github.com/virushuo/actionne-twitter) 45 | * 复制 actionne_twitter-0.1.0-SNAPSHOT.jar 到 /YOUR_PATH/plugins 目录 46 | 47 | ### DSL脚本 48 | 49 | 在/YOUR_PATH/scripts 下建立规则脚本,如 myscript.act 50 | 51 | 一个最简单的脚本,必须包含以下文件头: 52 | 53 | ``` 54 | Ver 1.0.0 55 | Namespace huoju/actionne_twitter 56 | Desc testsnsrules 57 | ``` 58 | 59 | Namespaces: 任意的用户名字/插件名 60 | Desc 任意描述字符 61 | 62 | 下面可以开始写自己的规则了: 63 | 64 | ``` 65 | Do delete created_at laterthan 12 hour category = str:reply 66 | ``` 67 | 68 | 这条规则会调用插件中的 delete 函数,条件是: created_at早于12个小时,并且category是reply。其中 created_at/category等字段都是在插件的输入数据集中定义的,随后会详述。 69 | 70 | 规则条件可以任意组合,比如更复杂一点的: 71 | 72 | ``` 73 | Do delete created_at laterthan 12 hour category = str:reply favorite_count < 20 id != str:1171575426201862144 id != str:1171576952588853248 id != str:1171572788785795074 74 | 75 | ``` 76 | 这条规则会调用插件中的 delete 函数,条件是: created_at早于12个小时,并且category是reply,并且favorite_count 小于 20,并且id 不是 1171575426201862144 并且id 不是 str:1171576952588853248 并且id 不是 str:1171572788785795074。 77 | 78 | 所有的条件之间都是 And (并且)的关系。 79 | 80 | 这里提供一个更复杂的脚本例子: [myscript.act](https://github.com/virushuo/actionne/blob/master/examples/scripts/myscript.act) 81 | 82 | 这个例子使用了4条触发删除动作的规则,分别为: 83 | 84 | 1. 删除 创建超过12小时 并且 like数量小于20 的全部回复 85 | 2. 删除 创建超过24小时 的全部retweet 86 | 3. 删除 创建超过24小时 并且 like数量小于20 并且 id不为"824653" (保留它的原因是我想保留我发过的第一条推) 并且不包含 #k 或者 #t 这两个标记的推 87 | 4. 和3类似,只是在96小时的时候改变一些参数 88 | 89 | ### 配置文件 90 | 91 | 配置文件用来定义运行周期,脚本名称,插件所需的环境变量和参数等。 92 | 93 | 一个配置文件的例子: 94 | 95 | ``` 96 | { :actionne_twitter { 97 | :consumer-key "" 98 | :consumer-secret "" 99 | :user-token "" 100 | :user-token-secret "" 101 | :screen_name "YOURNAME" 102 | :search_term "from:YOURNAME" 103 | :watching "3 days" 104 | :first_tweet "2006-12-01" 105 | :lastest_tweet "2019-12-31" 106 | :backup true 107 | :dryloop false 108 | } 109 | :scripts{ 110 | :myscript 20 111 | } 112 | 113 | } 114 | ``` 115 | :actionne_twitter 是对应名称插件的配置项。配置项细节可以在 actionne_twitter 项目中看到。 116 | :scripts 里面列出了脚本和对应脚本的运行周期。这里使用的配置 :myscript 前面保存的 myscript.act 的名字,20是每20秒运行一次。 117 | 118 | ## 运行 119 | 120 | 终于可以运行了。再次运行 121 | 122 | java -Dhomedir="/YOUR_PATH" -jar actionne-0.1.0-SNAPSHOT-standalone.jar 123 | 124 | 如果一切正常,会看到logs中提示已经正确加载了插件: 125 | 126 | INFO: loading... /YOUR_PATH/plugins/actionne_twitter.jar 127 | 128 | 这里我们使用了actionne_twitter插件,这个插件会先进行权限检查: 129 | 130 | INFO: call actionne_twitter/core.startcheck for account: YOURNAME 131 | 132 | 如果没有登录信息,会提示oauth授权连接,允许访问之后把屏幕输出的pincode贴回终端回车,actionne_twitter会保存登录信息,之后进入正常运行状态。 133 | 134 | 此时在 /YOU_PATH/data 目录下面会出现用户数据,比如 YOUNAME-actionne_twitter-session.clj 用来记录插件所需的状态数据。 .backup.log 备份数据(如果在actionne_twitter配置中允许了备份)。 135 | 136 | 只要actionne保持运行,它就会按照配置文件定义的运行周期反复获取数据,检查规则,执行动作。 137 | 138 | Tips: 如果在脚本中设置一个永远满足的条件,对应的动作就会永远运行。利用这个办法,甚至可以删光自己全部历史twitter内容。(actionne_twitter插件提供了从web获取历史tweets的方式,不受twitter api的3200条tweets限制。 139 | 140 | ## License 141 | 142 | Distributed under the Eclipse Public License. 143 | -------------------------------------------------------------------------------- /doc/Installation-for-dummies.md: -------------------------------------------------------------------------------- 1 | # Install `actionne-twitter` for Dummies 2 | 3 | An absolutely user-friendly guide for dummies to deploy `actionne-twitter`. 4 | 5 | ## Prerequisites 6 | 7 | * Clojure >= 1.9.0 8 | * JDK >= 11 9 | * A valid [Twitter Developer account](https://developer.twitter.com/en) with at least one active app 10 | 11 | ## Step 1: Get the Essentials 12 | 13 | Once confirmed that prerequisites have all been satisfied, you'll need to download the following executes: 14 | 15 | * [actionne_twitter.jar](https://github.com/virushuo/actionne-twitter/releases): the actual actionne Twitter plugin 16 | * [actionne-0.1.0-SNAPSHOT-standalone.jar](https://github.com/virushuo/actionne/releases): the underlying actionne framework 17 | 18 | ## Step 2: Run `actionne` & Confirm Folder Structure 19 | 20 | Command to run actionne: `java -Dhomedir="/YOUR_PATH" -jar actionne-0.1.0-SNAPSHOT-standalone.jar` 21 | 22 | actionne will create a directory called `/YOUR_PATH` and exit with errors due to missing configuration files. Before calling actionne again, ensure that `/YOUR_PATH` has a similar structure as below: 23 | 24 | ``` 25 | ├── /YOUR_PATH 26 | │ ├── config 27 | │ │ ├── default.edn 28 | │ ├── data 29 | │ ├── plugins 30 | │ │ ├── actionne_twitter.jar 31 | │ ├── scripts 32 | │ │ ├── xxxx.act 33 | │ └── actionne-0.1.0-SNAPSHOT-standalone.jar 34 | ``` 35 | 36 | ## Step 3: Edit Files 37 | 38 | ### Scripts 39 | Rule scripts should be saved under `scripts/`. The header must start with the following lines (case sensitive): 40 | 41 | ``` 42 | Ver 1.0.0 43 | Namespace your_user_name/actionne_twitter 44 | Desc your_rule 45 | ``` 46 | 47 | Substitute "your_user_name" and "your_rule" above based on your needs. 48 | 49 | An example rule: `Do delete created_at laterthan 12 hour category = str:reply` - this rule will call the `delete` function from the `actionne_twitter` plugin, and remove tweets that meet the criteria: 50 | 51 | * Tweets that are created 12 hours ago, AND 52 | * Category of the tweet = "reply" 53 | 54 | All conditions within a rule are joint through AND. This script ([myscript.act](../examples/scripts/myscript.act)) contains more complex instances. 55 | 56 | ### Config Files 57 | 58 | The purpose of config files is to determine the name of the script, execution cycle, environment variables, etc. It should be stored under the `config/` folder with filename `default.edn` (otherwise, the code will panic and exit with errors). 59 | 60 | A sample, bare-minimum config file: 61 | 62 | ``` 63 | { :actionne_twitter { 64 | :consumer-key "your-consumer-key-here" 65 | :consumer-secret "your-consumer-secret-here" 66 | :screen_name "your-screen-name" 67 | :watching "5 days" 68 | :backup true 69 | :dryloop false 70 | } 71 | :scripts{ 72 | :myscript 3600 73 | } 74 | 75 | ``` 76 | The `:actionne_twitter` block specifies config details for the `actionne_twitter` plugin. 77 | 78 | * `consumer-key` and `consumer-secret` could be obtained through [Twitter developer site](https://developer.twitter.com/en) (a valid app is required). Substitute your `screen_name` (e.g. `@momopirin`) after `:screen_name`. For testing purpose, you could set `:dryloop true` so that the tool will only detect tweets that meet the designated criteria, but not take any actions (e.g. delete tweets). 79 | 80 | Similarly, `:scripts` block stores information about available scripts. The example above assumes that there is a file called `myscript.act`, under `/YOUR_PATH/scripts`. The script should be run every hour (a.k.a. 3600 seconds). 81 | 82 | ## Step 4: Run `actionne`, for Real... 83 | 84 | `cd` into `/YOUR_PATH`, and run the command again: `java -Dhomedir="/YOUR_PATH" -jar actionne-0.1.0-SNAPSHOT-standalone.jar` 85 | 86 | If everything is set up correctly, you should see the following prompt on the screen: 87 | 88 | `INFO: loading... /YOUR_PATH/plugins/actionne_twitter.jar` 89 | 90 | It will then redirect you to an URL to finish Twitter OAuth setup. Log in with your credential, copy & paste the passcode into the command line. 91 | 92 | actionne should be up and running then! You should be able to find user data under `/YOUR_PATH/data/`. As long as the session remains active, the script is supposed to check & perform corresponding actions continuously, subject to the cycle time stated in the configuration file. 93 | 94 | ## Tips 95 | 96 | * If there exists a rule that will never be False (e.g. endless loop), its matching action will run forever. You may utilize this to remove all your tweets. actionne_twitter is not subject to the 3,200 tweet limit from the official Twitter API; instead, it obtains tweets from the web interface. 97 | * Keep in mind that reserved words (e.g. `Namespace`) in a config file is case sensitive. Misspelling reserved words (e.g. `namespace`) will invoke weird errors, e.g. `nth not supported on this type`. 98 | * Script name in the config file must match with the name of the `.act` file under `/YOUR_PATH/scripts`. 99 | * A valid pair of `consumer-key` and `consumer-secret` is required. There are some unofficial consumer keys in the wild; try them at your risks. 100 | 101 | ## Ways to Keep it Running 102 | 103 | There are multiple ways to do so. I just listed a couple that I've tried so far: 104 | 105 | 1. Open a console window and have it running. Remember to re-run the command if you lose connection to the console, though. 106 | 2. Have the program running in `tmux` ([cheatsheet](https://tmuxcheatsheet.com/)). Not fun if you're a restart freak like me! 107 | 3. Set up `systemctl`. **Recommend, pretty stable.** 108 | 4. Ask the author to come up with a Docker version. Hopefully he will create one soon :) 109 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to actionne 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /examples/config/default.edn: -------------------------------------------------------------------------------- 1 | { :actionne_twitter { 2 | :consumer-key "" 3 | :consumer-secret "" 4 | :user-token "" 5 | :user-token-secret "" 6 | :screen_name "virushuo" 7 | :watching "5 days" 8 | :backup true 9 | :dryloop false 10 | } 11 | :scripts{ 12 | :myscript 60 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /examples/scripts/myscript.act: -------------------------------------------------------------------------------- 1 | Ver 1.0.0 2 | Namespace huoju/actionne_twitter 3 | Desc mytwitter 4 | Do delete created_at laterthan 12 hour category = str:reply favorite_count < 20 5 | Do delete created_at laterthan 24 hour category = str:retweet 6 | Do delete created_at laterthan 24 hour category = str:tweet id != str:824653 favorite_count < 20 text notinclude str:#k text notinclude str:#t 7 | Do delete created_at laterthan 96 hour category = str:tweet id != str:824653 favorite_count < 100 text notinclude str:#k text notinclude str:#t 8 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject actionne "0.1.0-SNAPSHOT" 2 | :description "Protect your privacy, clean up your sns accounts " 3 | :url "http://example.com/FIXME" 4 | :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 5 | :url "https://www.eclipse.org/legal/epl-2.0/"} 6 | :main actionne.core 7 | :aot [actionne.core] 8 | :profiles {:uberjar {:aot :all} 9 | :dev {:resource-paths ["config/dev"]} 10 | :prod {:resource-paths ["config/prod"]} } 11 | :plugins [ [lein-ring "0.12.0"] [lein-cljfmt "0.6.0"]] 12 | :dependencies [[org.clojure/clojure "1.9.0"], 13 | [instaparse "1.4.9"] , 14 | [metosin/compojure-api "2.0.0-alpha30"] 15 | [ring/ring-jetty-adapter "1.6.3"] 16 | [com.cerner/clara-rules "0.18.0"] 17 | [tea-time "1.0.1"] 18 | [twttr "3.2.2"] 19 | [clj-http "3.10.0"] 20 | [yogthos/config "1.1.1"] 21 | [org.tcrawley/dynapath "1.0.0"] 22 | [org.clojure/tools.namespace "0.2.11"] 23 | [org.clojure/java.classpath "0.2.3"] 24 | ] 25 | :repl-options {:init-ns actionne.core}) 26 | -------------------------------------------------------------------------------- /src/actionne/classpath.clj: -------------------------------------------------------------------------------- 1 | (ns actionne.classpath 2 | "This is the add-classpath function from Pomegranate 1.0.0, extracted so we 3 | don't need to pull in Aether." 4 | (:refer-clojure :exclude [add-classpath]) 5 | (:require [dynapath.util :as dp] 6 | [clojure.java.io :as io])) 7 | 8 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 9 | ;; Pomegranate 10 | 11 | (defn ensure-compiler-loader 12 | "Ensures the clojure.lang.Compiler/LOADER var is bound to a DynamicClassLoader, 13 | so that we can add to Clojure's classpath dynamically." 14 | [] 15 | (when-not (bound? Compiler/LOADER) 16 | (.bindRoot Compiler/LOADER (clojure.lang.DynamicClassLoader. (clojure.lang.RT/baseLoader))))) 17 | 18 | (defn- classloader-hierarchy 19 | "Returns a seq of classloaders, with the tip of the hierarchy first. 20 | Uses the current thread context ClassLoader as the tip ClassLoader 21 | if one is not provided." 22 | ([] 23 | (ensure-compiler-loader) 24 | (classloader-hierarchy (deref clojure.lang.Compiler/LOADER))) 25 | ([tip] 26 | (->> tip 27 | (iterate #(.getParent %)) 28 | (take-while boolean)))) 29 | 30 | (defn- modifiable-classloader? 31 | "Returns true iff the given ClassLoader is of a type that satisfies 32 | the dynapath.dynamic-classpath/DynamicClasspath protocol, and it can 33 | be modified." 34 | [cl] 35 | (dp/addable-classpath? cl)) 36 | 37 | (defn add-classpath 38 | "A corollary to the (deprecated) `add-classpath` in clojure.core. This implementation 39 | requires a java.io.File or String path to a jar file or directory, and will attempt 40 | to add that path to the right classloader (with the search rooted at the current 41 | thread's context classloader)." 42 | ([jar-or-dir classloader] 43 | (if-not (dp/add-classpath-url classloader (.toURL (.toURI (io/file jar-or-dir)))) 44 | (throw (IllegalStateException. (str classloader " is not a modifiable classloader"))))) 45 | ([jar-or-dir] 46 | (let [classloaders (classloader-hierarchy)] 47 | (if-let [cl (filter modifiable-classloader? classloaders)] 48 | ;; Add to all classloaders that allow it. Brute force but doesn't hurt. 49 | (run! #(add-classpath jar-or-dir %) cl) 50 | (throw (IllegalStateException. (str "Could not find a suitable classloader to modify from " 51 | classloaders))))))) 52 | 53 | ;; /Pomegranate 54 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 55 | -------------------------------------------------------------------------------- /src/actionne/core.clj: -------------------------------------------------------------------------------- 1 | (ns actionne.core 2 | (:gen-class) 3 | (:require [clojure.string :as string] 4 | [clojure.edn :as edn] 5 | [clojure.tools.logging :as log] 6 | [config.core :refer [env]] 7 | [instaparse.core :as insta] 8 | [clara.rules.accumulators :as acc] 9 | [clara.rules :refer :all] 10 | [tea-time.core :as tt] 11 | [compojure.api.sweet :refer [api routes]] 12 | [compojure.api.exception :as ex] 13 | [ring.util.http-response :refer :all] 14 | [ring.adapter.jetty :refer [run-jetty]] 15 | [dynapath.util :as dynapath] 16 | [clojure.java [classpath :as classpath]] 17 | [clojure.tools.namespace.find :as ns-find] 18 | [clojure.java.io :as io] 19 | [actionne.classpath]) 20 | (:import (java.util Date Locale) java.io.PushbackReader)) 21 | 22 | (defrecord Msgs [id obj original]) 23 | 24 | (defrecord ActionMsgs [action id original]) 25 | 26 | (defquery get-actionmsgs [] 27 | [?msg <- ActionMsgs]) 28 | 29 | (defquery get-msgs [] 30 | [?msg <- Msgs]) 31 | 32 | (def parser 33 | (insta/parser 34 | " = ver namespace [ desc | action ]+ 35 | ver = <'Ver'> space MAJOR <'.'> MINOR <'.'> PATCH [META] 36 | namespace = <'Namespace'> space NSIDENTIFIER <'/'> NSLOCALNAME 37 | desc = <'Desc'> space utf8stre 38 | action = <'Do'> string space clause [clause]* 39 | clause = string symbol [ digit | strvar ] ?[unit] 40 | unit = 'minute' | 'day' | 'hour'; 41 | logic = 'and' | 'or' | 'not'; 42 | symbol = '>' | '<' | '=' | '>=' | '<=' | '!=' | 'include' | 'notinclude' | 'laterthan'; 43 | = #'[0-9]\\d?(?:\\.\\d{1,2})?%'; 44 | = #'[A-Za-z0-9_-]+'; 45 | = <#'[ ]+'>; 46 | = #'([^\r\n\"\\\\]|\\s\\\\.,)+'; 47 | = #'([^\r\n\"]|\\s\\\\.,)+'; 48 | = #'([^\r\n\" ]|\\s\\\\.,)+'; 49 | strvar = <'str:'> utf8strwithoutspace 50 | = #'[0-9]+(\\.[0-9]+)?'; 51 | digit = #'[0-9]+'; 52 | MAJOR = digit 53 | MINOR = digit 54 | PATCH = digit 55 | META = #'[0-9A-Za-z-]' 56 | NSIDENTIFIER = string 57 | NSLOCALNAME = string 58 | = #'[\r\n]+'; 59 | " 60 | :auto-whitespace :standard)) 61 | 62 | (defn print-desc [input] 63 | (println (str "desc: " input))) 64 | 65 | (def df 66 | (java.text.SimpleDateFormat. "EEE MMM dd HH:mm:ss ZZZZZ yyyy" (Locale. "english"))) 67 | 68 | (def time-to-minutes 69 | {"minute" `1 70 | "minutes" `1 71 | "hour" `60 72 | "hours" `60 73 | "day" `1440 74 | "days" `1440}) 75 | 76 | (defn timelaterthan [left right unit] 77 | (let [offset (- (.getTime (new java.util.Date)) left)] 78 | (if (>= (quot offset (* 60 1000)) (* right (time-to-minutes unit))) 79 | true 80 | false))) 81 | 82 | (defn notinclude [left right] 83 | (not (string/includes? left right))) 84 | 85 | (def symbol-operator {"=" `= 86 | ">" `> 87 | "<" `< 88 | ">=" `>= 89 | "<=" `<= 90 | "!=" `not= 91 | "include" `string/includes? 92 | "notinclude" `notinclude 93 | "laterthan" `timelaterthan}) 94 | 95 | (def logic-operator {"and" `:and 96 | "or" `:or 97 | "not" `:not}) 98 | 99 | (def msg-types 100 | {"msgs" Msgs}) 101 | 102 | (def transform-options 103 | {:desc (fn [thedesc] 104 | {:name "desc" 105 | :lhs () 106 | :rhs `(print-desc ~thedesc)}) 107 | :namespace (fn [identifier localname] (into {} [identifier localname])) 108 | :unit read-string 109 | :clause (fn [property operator value & unit] 110 | {:type Msgs 111 | :constraints [(list `= (symbol "?msgid") (symbol "id")) (list `= (symbol "?original") (symbol "original")) (remove nil? (list operator (list (keyword property) (symbol "obj")) value (if (not= nil unit) (clojure.string/join unit))))]}) 112 | 113 | :symbol symbol-operator 114 | :strvar (fn [input] 115 | :lhs () 116 | :rhs `~input) 117 | :logic logic-operator 118 | :digit #(Integer/parseInt %) 119 | :action (fn [action & clauses] 120 | {:lhs clauses 121 | :rhs `(insert! (->ActionMsgs ~action ~(symbol "?msgid") ~(symbol "?original")))})}) 122 | 123 | (defn doaction [config dslns msg] 124 | (let [{identifier :NSIDENTIFIER localname :NSLOCALNAME} dslns] 125 | (let [{{action :action id :id original :original} :?msg} msg] 126 | ((resolve (symbol (str localname ".core/" action))) config id original) 127 | (log/info (str "action: " identifier "." localname "/" action " " id))))) 128 | 129 | (defn expand-home [s] 130 | (if (.startsWith s "~") 131 | (clojure.string/replace-first s "~" (System/getProperty "user.home")) 132 | s)) 133 | 134 | (def homedir 135 | (expand-home (or (:homedir env) "~/actionne"))) 136 | 137 | (defn startcheck [] 138 | (if (not (or (.exists (io/file (str homedir "/data"))) (.exists (io/file (str homedir "/config"))) (.exists (io/file (str homedir "/plugins"))) (.exists (io/file (str homedir "/scripts"))))) 139 | (do 140 | (.mkdirs (io/file (str homedir "/data"))) 141 | (.mkdirs (io/file (str homedir "/config"))) 142 | (.mkdirs (io/file (str homedir "/plugins"))) 143 | (.mkdirs (io/file (str homedir "/scripts"))))) 144 | 145 | (log/info (str "using " homedir " as homedir"))) 146 | 147 | (defn read-config-file [f] 148 | (try 149 | (when-let [url (or (io/resource f) (io/file f))] 150 | (with-open [r (-> url io/reader PushbackReader.)] 151 | (edn/read r))) 152 | (catch Exception e 153 | (log/warn (str "WARNING: failed to parse " f " " (.getLocalizedMessage e)))))) 154 | 155 | (defn load-config [& name] 156 | (let [configname (if (nil? name) "default.edn" (first name))] 157 | (read-config-file (str homedir "/config/" configname)))) 158 | 159 | (defn load-scripts [scripts] 160 | (map (fn [item] 161 | {:name (key item) :interval (val item) :script (slurp (str homedir "/scripts/" (name (first item)) ".act"))}) scripts)) 162 | 163 | (defn filteritems [transformedscript items] 164 | (let [facts (map (fn [item] 165 | (->Msgs (:id item) (:object item) (:original item))) items)] 166 | (let [session (-> (mk-session 'actionne.core transformedscript) 167 | (insert-all (into [] facts)) 168 | (fire-rules))] 169 | (query session get-actionmsgs)))) 170 | 171 | (defn runtask [pluginname config transformedscript] 172 | 173 | (let [[ver dslns] transformedscript] 174 | ((resolve (symbol (str pluginname ".core/before"))) ((keyword pluginname) config)) 175 | (let [items ((resolve (symbol (str pluginname ".core/run"))) ((keyword pluginname) config))] 176 | (let [actionmsgs (filteritems transformedscript items)] 177 | (try 178 | (do 179 | (dorun (map (fn [msg] (doaction config dslns msg)) actionmsgs)) 180 | ((resolve (symbol (str pluginname ".core/success"))) ((keyword pluginname) config))) 181 | (catch Exception e 182 | (log/error (str "doaction error: " (.getLocalizedMessage e))) 183 | (prn e))) 184 | (log/info "task done."))))) 185 | 186 | (defmacro starttaskmacro [pluginname interval config transformedscript] 187 | `(let [pluginname# ~pluginname interval# ~interval config# ~config transformedscript# ~transformedscript] 188 | (def pluginname# (tt/every! interval# (bound-fn [] 189 | (runtask pluginname# config# transformedscript#)))))) 190 | (defn scripttransform [script] 191 | (let [parse-tree (parser script)] 192 | (insta/transform transform-options parse-tree))) 193 | 194 | (defn -main [& args] 195 | (log/info "start...") 196 | (startcheck) 197 | (tt/start!) 198 | (let [config (load-config)] 199 | (let [scripts (load-scripts (:scripts config))] 200 | (mapv (fn [scriptobj] 201 | (let [transformedscript (scripttransform (:script scriptobj))] 202 | (let [[ver dslns] transformedscript] 203 | (let [pluginname (:NSLOCALNAME dslns)] 204 | (log/info (str "loading... " homedir "/plugins/" pluginname ".jar")) 205 | (actionne.classpath/add-classpath (str homedir "/plugins/" pluginname ".jar")) 206 | (require (symbol (str pluginname ".core"))) 207 | (if (= true ((resolve (symbol (str pluginname ".core/startcheck"))) ((keyword pluginname) config))) 208 | (starttaskmacro pluginname (:interval scriptobj) config transformedscript) 209 | (log/error (str pluginname " plugin startcheck error: "))) 210 | (runtask pluginname config transformedscript))))) 211 | scripts)))) 212 | 213 | 214 | -------------------------------------------------------------------------------- /test/actionne/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns actionne.core-test 2 | (:require [clojure.test :refer :all] 3 | [actionne.core :refer :all] 4 | [clara.rules :refer :all])) 5 | 6 | (def rules1 7 | "Ver 1.0.0 8 | Namespace huoju/actionne_twitter 9 | Desc testsnsrules 10 | Do delete created_at laterthan 1 hour category = str:reply favorite_count < 20 id != str:1111111111111111111 id != str:1111111111111111112 11 | ") 12 | 13 | (def items1 14 | [{:id "1173326296325251083", :object {:id "1173326296325251083", :favorite_count 1, :retweet_count 0, :text "this is my test reply 1", :created_at 1568577763000, :media_urls [], :category "reply"} :original {}}, 15 | {:id "1173323074497363969", :object {:id "1173293074497363969", :favorite_count 4, :retweet_count 0, :text "this is my test tweet 2" :created_at 1568569842000, :media_urls [], :category "tweet"} :original {}} ] 16 | ) 17 | 18 | (def items2 19 | [{:id "1173326296325251083", :object {:id "1111111111111111111", :favorite_count 1, :retweet_count 0, :text "this is my test reply 1", :created_at 1568577763000, :media_urls [], :category "reply"} :original {}}, 20 | {:id "1173323074497363969", :object {:id "1173293074497363969", :favorite_count 4, :retweet_count 0, :text "this is my test tweet 2" :created_at 1568569842000, :media_urls [], :category "tweet"} :original {}} ] 21 | ) 22 | 23 | 24 | (deftest verify_item1 25 | (testing "item1 match delete constraints in rules1" 26 | (let [transformedscript (scripttransform rules1)] 27 | (let [facts (map (fn [item] (->Msgs (:id item) (:object item) (:original item))) items1)] 28 | (let [session (-> (mk-session 'actionne.core transformedscript) 29 | (insert-all (into [] facts)) 30 | (fire-rules))] 31 | (let [actionmsgs (query session get-actionmsgs)] 32 | (let [{{action :action id :id original :original} :?msg} (first actionmsgs)] 33 | (is (= "1173326296325251083" id)) 34 | ))))))) 35 | 36 | (deftest verify_item12 37 | (testing "item1 not match any constraints in rules1" 38 | (let [transformedscript (scripttransform rules1)] 39 | (let [facts (map (fn [item] (->Msgs (:id item) (:object item) (:original item))) items2)] 40 | (let [session (-> (mk-session 'actionne.core transformedscript) 41 | (insert-all (into [] facts)) 42 | (fire-rules))] 43 | (let [actionmsgs (query session get-actionmsgs)] 44 | (let [{{action :action id :id original :original} :?msg} (first actionmsgs)] 45 | (is (= nil id)) 46 | ))))))) 47 | 48 | --------------------------------------------------------------------------------