├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── actions.go ├── auth.go ├── comments.go ├── const.go ├── doc.go ├── examples ├── comments │ └── main.go ├── composing │ └── main.go ├── demo │ ├── demo.svg │ └── reddit.go ├── stream_comment_replies │ └── main.go ├── stream_comments │ └── main.go ├── stream_submissions │ └── main.go └── submissions │ └── main.go ├── go.mod ├── go.sum ├── messages.go ├── mira.go ├── mira.png ├── mod.go ├── models ├── comment.go ├── comment_struct.go ├── comment_struct_test.go ├── commentlisting.go ├── commentlisting_struct.go ├── commentlisting_struct_test.go ├── me.go ├── me_struct.go ├── me_struct_test.go ├── modqueue.go ├── modqueue_struct.go ├── modqueue_struct_test.go ├── modqueue_test.go ├── post.go ├── post_struct.go ├── post_struct_test.go ├── post_test.go ├── redditor.go ├── redditor_struct.go ├── redditor_struct_test.go ├── reports.go ├── reports_struct.go ├── reports_struct_test.go ├── reports_test.go ├── submission.go ├── submission_struct.go ├── submission_struct_test.go ├── subreddit.go ├── subreddit_struct.go ├── subreddit_struct_test.go ├── subreddit_test.go ├── tests │ ├── comment.json │ ├── commentlisting.json │ ├── me.json │ ├── modqueue.json │ ├── postlisting.json │ ├── redditor.json │ ├── reports.json │ ├── submission.json │ └── subreddit.json └── user_reports.go ├── modqueue.go ├── reddit.go ├── reddit_interface.go ├── reddit_struct.go ├── reports.go ├── streaming.go ├── submissions.go └── utils.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | test: 6 | name: Test 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.17 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.17 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | - name: Test 19 | run: go test -v -bench -count=1 ./models 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | login.conf 2 | *.saves 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Enviornment specific files 17 | .vscode/ 18 | *.nix -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![mira](./mira.png) 2 | 3 |
4 | 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/thecsw/mira/v4)](https://goreportcard.com/report/github.com/thecsw/mira/v4) 6 | [![GoDoc](https://godoc.org/github.com/thecsw/mira/v4?status.svg)](https://godoc.org/github.com/thecsw/mira/v4) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 8 | 9 |
10 | 11 | For full documentation, please see the [Godoc page](https://godoc.org/github.com/thecsw/mira/v4) 12 | 13 | *mira* is a Reddit Api Wrapper written in beautiful Go. Featured in issue [306](https://golangweekly.com/issues/306) of Golang Weekly 🚀 14 | 15 | It is super simple to use the bot as we also provide you with simple but fully extensive 16 | interfaces. Currently, *mira* is a project that is considered more or less complete. All 17 | main functionality, such as streaming, data manipulation, data request, submissions, links, 18 | etc. are fully implemented. *mira* can be extended to use any Reddit API endpoint. More 19 | details at the bottom of this page. 20 | 21 | ## Demo 22 | 23 | ![Demo](./examples/demo/demo.svg) 24 | 25 | Two quick notes: all actions should be done via `Reddit` struct, I thought it would make it 26 | simpler to work with. Secondly, all actions require the objects full `thing_id`, so you have 27 | to use `GetId()` to get that id. Every struct has that method implemented and it will return 28 | a string in the form of `t[1-6]_[a-z0-9]{5}`. Refer to the following table for the classifications 29 | of the structs. 30 | 31 | **Type Prefixes** 32 | 33 | | Prefix | Type | 34 | |--------|----------------------------------| 35 | | t1 | Comment | 36 | | t2 | Redditor | 37 | | t3 | Submission, PostListing contents | 38 | | t4 | Message (NOT IMPLEMENTED) | 39 | | t5 | Subreddit | 40 | | t6 | Award (NOT IMPLEMENTED) | 41 | 42 | ## Config file 43 | 44 | The config file structure is very simple: 45 | 46 | ``` 47 | login.conf 48 | ---------- 49 | CLIENT_ID = 50 | CLIENT_SECRET = 51 | USERNAME = 52 | PASSWORD = 53 | USER_AGENT = 54 | ``` 55 | 56 | ``` go 57 | r, err := mira.Init(mira.ReadCredsFromFile("login.conf")) 58 | ``` 59 | 60 | ## Environment setup 61 | 62 | Mira also works with environmental variables, here is an example from docker-compose 63 | 64 | ``` 65 | environment: 66 | - BOT_CLIENT_ID=hunteoahtnhnt432 67 | - BOT_CLIENT_SECRET=ehoantehont4ht34hnt332 68 | - BOT_USER_AGENT='u/mytestbot developed by thecsw' 69 | - BOT_USERNAME=mytestbot 70 | - BOT_PASSWORD=verygoodpassword 71 | ``` 72 | 73 | And the login will look like this: 74 | 75 | ``` go 76 | r, err := mira.Init(mira.ReadCredsFromEnv()) 77 | ``` 78 | 79 | Or you can always just fill in the values directly. 80 | 81 | ## Examples 82 | 83 | Note: Error checking is omitted for brevity. 84 | 85 | ### Streaming 86 | 87 | Streaming new submissions is very simple! *mira* supports streaming comment replies, 88 | mentions, new subreddit's/redditor's comments, and new subreddit's/redditor's submissions. 89 | 90 | ``` go 91 | // r is an instance of *mira.Reddit 92 | r, err := mira.Init(mira.ReadCredsFromFile("login.conf")) 93 | 94 | // Start streaming my comment replies 95 | c, err := r.StreamCommentReplies() 96 | for { 97 | msg := <-c 98 | r.Comment(msg.GetId()).Reply("I got your message!") 99 | } 100 | 101 | // Start streaming my mentions 102 | // Start streaming my comment replies 103 | c, err := r.StreamMentions() 104 | for { 105 | msg := <-c 106 | r.Comment(msg.GetId()).Reply("I got your mention of me!") 107 | } 108 | 109 | // Start streaming subreddits' submissions 110 | c, err := r.Subreddit("tifu", "wholesomememes").StreamSubmissions() 111 | for { 112 | post := <-c 113 | r.Submission(post.GetId()).Save("hello there") 114 | } 115 | 116 | // NOTE: Second value is the stop channel. Send a true value 117 | // to the stop channel and the goroutine will return. 118 | // Basically, `stop <- true` 119 | 120 | // Start streaming subreddits' comments 121 | c, err := r.Subreddit("all").StreamComments() 122 | for { 123 | msg := <-c 124 | r.Comment(msg.GetId()).Reply("my reply!") 125 | } 126 | 127 | // Start streaming redditor's submissions 128 | c, err := r.Redditor("thecsw").StreamSubmissions() 129 | for { 130 | post := <-c 131 | r.Submission(post.GetId()).Save("hello there") 132 | } 133 | 134 | // Start streaming redditor' comments 135 | c, err := r.Redditor("thecsw").StreamComments() 136 | for { 137 | msg := <-c 138 | r.Comment(msg.GetId()).Reply("my reply!") 139 | } 140 | ``` 141 | 142 | ### Submitting, Commenting, Replying, and Editing 143 | 144 | It is very easy to post a submission, comment on it, reply to a message, or 145 | edit a comment. 146 | 147 | ``` go 148 | package main 149 | 150 | import ( 151 | "fmt" 152 | 153 | "github.com/thecsw/mira/v4" 154 | ) 155 | 156 | // Error checking is omitted for brevity 157 | func main() { 158 | r, err := mira.Init(mira.ReadCredsFromFile("login.conf")) 159 | 160 | // Make a submission 161 | post, err := r.Subreddit("mysubreddit").Submit("mytitle", "mytext") 162 | 163 | // Comment on our new submission 164 | comment, err := r.Submission(post.GetId()).Save("mycomment") 165 | 166 | // Reply to our own comment 167 | reply, err := r.Comment(comment.GetId()).Reply("myreply") 168 | 169 | // Delete the reply 170 | r.Comment(reply.GetId()).Delete() 171 | 172 | // Edit the first comment 173 | newComment, err := r.Comment(comment.GetId()).Edit("myedit") 174 | 175 | // Show the comment's body 176 | fmt.Println(newComment.GetBody()) 177 | } 178 | ``` 179 | 180 | ### Composing a message 181 | 182 | We can also send a message to another user! 183 | 184 | ``` go 185 | package main 186 | 187 | import ( 188 | "github.com/thecsw/mira/v4" 189 | ) 190 | 191 | func main() { 192 | r, err := mira.Init(mira.ReadCredsFromFile("login.conf")) 193 | 194 | r.Redditor("myuser").Compose("mytitle", "mytext") 195 | } 196 | ``` 197 | 198 | ### Going through hot, new, top, rising, controversial, and random 199 | 200 | You can also traverse through a number of submissions using 201 | one of our methods. 202 | 203 | ``` go 204 | package main 205 | 206 | import ( 207 | "fmt" 208 | 209 | "github.com/thecsw/mira/v4" 210 | ) 211 | 212 | func main() { 213 | r, err := mira.Init(mira.ReadCredsFromFile("login.conf")) 214 | sort := "top" 215 | var limit int = 25 216 | duration := "all" 217 | subs, err := r.Subreddit("all").Submissions(sort, duration, limit) 218 | for _, v := range subs { 219 | fmt.Println("Submission Title: ", v.GetTitle()) 220 | } 221 | } 222 | ``` 223 | 224 | ### Getting reddit info 225 | 226 | You can extract info from any reddit ID using mira. The returned value is an 227 | instance of mira.MiraInterface. 228 | 229 | ``` go 230 | package main 231 | 232 | import ( 233 | "fmt" 234 | 235 | "github.com/thecsw/mira/v4" 236 | ) 237 | 238 | func main() { 239 | r, err := mira.Init(mira.ReadCredsFromFile("login.conf")) 240 | me, err := r.Me().Info() 241 | comment, err := r.Comment("t1_...").Info() 242 | redditor, err := r.Redditor("t2_...").Info() 243 | submission, err := r.Submission("t3_...").Info() 244 | subreddit, err := r.Subreddit("t5_...").Info() 245 | } 246 | ``` 247 | 248 | Here is the interface: 249 | 250 | ``` go 251 | type MiraInterface interface { 252 | GetId() string 253 | GetParentId() string 254 | GetTitle() string 255 | GetBody() string 256 | GetAuthor() string 257 | GetName() string 258 | GetKarma() float64 259 | GetUps() float64 260 | GetDowns() float64 261 | GetSubreddit() string 262 | GetCreated() float64 263 | GetFlair() string 264 | GetUrl() string 265 | IsRoot() bool 266 | } 267 | ``` 268 | 269 | ## Mira Caller 270 | 271 | Surely, Reddit API is always developing and I can't implement all endpoints. It will be a bit of a bloat. 272 | Instead, you have accessto *Reddit.MiraRequest method that will let you to do any custom reddit api calls! 273 | 274 | Here is the signature: 275 | 276 | ``` go 277 | func (c *Reddit) MiraRequest(method string, target string, payload map[string]string) ([]byte, error) {...} 278 | ``` 279 | 280 | It is pretty straight-forward. The return is a slice of bytes. Parse it yourself. 281 | 282 | Here is an example of how Reddit.Reply() uses MiraRequest: 283 | 284 | NOTE: `checkType(...)` is a quick method to pop a value from the 285 | queue and make sure it's a valid value and type. For example, 286 | 287 | ``` go 288 | r.Comment("COMM1").Submission("SUBM1").Redditor("USER1") 289 | ``` 290 | 291 | will add elements to its internal queue, so that the layout is: 292 | 293 | ``` 294 | Enqueue-> 295 | redditor submission comment // type 296 | |BACK| -> |USER1| -> |SUBM1| -> |COMM1| -> |FRONT| // value 297 | Dequeue-> 298 | ``` 299 | 300 | So that when you run `r.checkType("comment")`, it will dequeue `COMM1` 301 | and return triplet `"COMM1", "comment", nil`. 302 | 303 | If you run `r.checkType("redditor")` (will fail because subm is at the end), 304 | you will get `"", "", "errors.New("the passed type...")` 305 | 306 | Here is an example of how you check that the last element to dequeue is 307 | a type that you're expecting: 308 | 309 | ``` go 310 | func (c *Reddit) Reply(text string) (models.CommentWrap, error) { 311 | ret := &models.CommentWrap{} 312 | // Second return is type, which is "comment" 313 | name, _, err := c.checkType("comment") 314 | if err != nil { 315 | return *ret, err 316 | } 317 | target := RedditOauth + "/api/comment" 318 | ans, err := c.MiraRequest("POST", target, map[string]string{ 319 | "text": text, 320 | "thing_id": name, 321 | "api_type": ApiTypeJson, 322 | }) 323 | json.Unmarshal(ans, ret) 324 | return *ret, err 325 | } 326 | ``` 327 | -------------------------------------------------------------------------------- /actions.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/thecsw/mira/v4/models" 8 | ) 9 | 10 | // Submit submits a submission to a subreddit. 11 | func (c *Reddit) Submit(title string, text string) (models.Submission, error) { 12 | ret := &models.Submission{} 13 | name, _, err := c.checkType(subredditType) 14 | if err != nil { 15 | return *ret, err 16 | } 17 | target := RedditOauth + "/api/submit" 18 | ans, err := c.MiraRequest(http.MethodPost, target, map[string]string{ 19 | "title": title, 20 | "sr": name, 21 | "text": text, 22 | "kind": "self", 23 | "resubmit": "true", 24 | "api_type": JsonAPI, 25 | }) 26 | json.Unmarshal(ans, ret) 27 | return *ret, err 28 | } 29 | 30 | // Reply replies to a comment with text. 31 | func (c *Reddit) Reply(text string) (models.CommentWrap, error) { 32 | ret := &models.CommentWrap{} 33 | name, _, err := c.checkType(commentType) 34 | if err != nil { 35 | return *ret, err 36 | } 37 | target := RedditOauth + "/api/comment" 38 | ans, err := c.MiraRequest(http.MethodPost, target, map[string]string{ 39 | "text": text, 40 | "thing_id": name, 41 | "api_type": JsonAPI, 42 | }) 43 | json.Unmarshal(ans, ret) 44 | return *ret, err 45 | } 46 | 47 | // ReplyWithID is the same as Reply but with explicit passing comment id. 48 | func (c *Reddit) ReplyWithID(name, text string) (models.CommentWrap, error) { 49 | ret := &models.CommentWrap{} 50 | target := RedditOauth + "/api/comment" 51 | ans, err := c.MiraRequest(http.MethodPost, target, map[string]string{ 52 | "text": text, 53 | "thing_id": name, 54 | "api_type": JsonAPI, 55 | }) 56 | json.Unmarshal(ans, ret) 57 | return *ret, err 58 | } 59 | 60 | // Save posts a comment to a submission. 61 | func (c *Reddit) Save(text string) (models.CommentWrap, error) { 62 | ret := &models.CommentWrap{} 63 | name, _, err := c.checkType(submissionType) 64 | if err != nil { 65 | return *ret, err 66 | } 67 | target := RedditOauth + "/api/comment" 68 | ans, err := c.MiraRequest(http.MethodPost, target, map[string]string{ 69 | "text": text, 70 | "thing_id": name, 71 | "api_type": JsonAPI, 72 | }) 73 | json.Unmarshal(ans, ret) 74 | return *ret, err 75 | } 76 | 77 | // SaveWithID is the same as Save but with explicitely passing. 78 | func (c *Reddit) SaveWithID(name, text string) (models.CommentWrap, error) { 79 | ret := &models.CommentWrap{} 80 | target := RedditOauth + "/api/comment" 81 | ans, err := c.MiraRequest(http.MethodPost, target, map[string]string{ 82 | "text": text, 83 | "thing_id": name, 84 | "api_type": JsonAPI, 85 | }) 86 | json.Unmarshal(ans, ret) 87 | return *ret, err 88 | } 89 | 90 | // Delete deletes whatever is next in the queue. 91 | func (c *Reddit) Delete() error { 92 | name, _, err := c.checkType(commentType, submissionType) 93 | if err != nil { 94 | return err 95 | } 96 | target := RedditOauth + "/api/del" 97 | _, err = c.MiraRequest(http.MethodPost, target, map[string]string{ 98 | "id": name, 99 | "api_type": JsonAPI, 100 | }) 101 | return err 102 | } 103 | 104 | // Edit will edit the next queued comment. 105 | func (c *Reddit) Edit(text string) (models.CommentWrap, error) { 106 | ret := &models.CommentWrap{} 107 | name, _, err := c.checkType(commentType, submissionType) 108 | if err != nil { 109 | return *ret, err 110 | } 111 | target := RedditOauth + "/api/editusertext" 112 | ans, err := c.MiraRequest(http.MethodPost, target, map[string]string{ 113 | "text": text, 114 | "thing_id": name, 115 | "api_type": JsonAPI, 116 | }) 117 | json.Unmarshal(ans, ret) 118 | return *ret, err 119 | } 120 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "bytes" 5 | b64 "encoding/base64" 6 | "encoding/json" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type Credentials struct { 14 | ClientID string 15 | ClientSecret string 16 | Username string 17 | Password string 18 | UserAgent string 19 | } 20 | 21 | // Authenticate returns *Reddit object that has been authed 22 | func Authenticate(c *Credentials) (*Reddit, error) { 23 | // URL to get access_token 24 | authURL := RedditBase + "api/v1/access_token" 25 | 26 | // Define the data to send in the request 27 | form := url.Values{} 28 | form.Add("grant_type", "password") 29 | form.Add("username", c.Username) 30 | form.Add("password", c.Password) 31 | 32 | // Encode the Authorization Header 33 | raw := c.ClientID + ":" + c.ClientSecret 34 | encoded := b64.StdEncoding.EncodeToString([]byte(raw)) 35 | 36 | // Create a request to allow customised headers 37 | r, err := http.NewRequest("POST", authURL, strings.NewReader(form.Encode())) 38 | if err != nil { 39 | return nil, err 40 | } 41 | // Customise request headers 42 | r.Header.Set("User-Agent", c.UserAgent) 43 | r.Header.Set("Authorization", "Basic "+encoded) 44 | 45 | // Create client 46 | client := &http.Client{} 47 | 48 | // Run the request 49 | response, err := client.Do(r) 50 | if err != nil { 51 | return nil, err 52 | } 53 | defer response.Body.Close() 54 | 55 | buf := new(bytes.Buffer) 56 | buf.ReadFrom(response.Body) 57 | 58 | data := buf.Bytes() 59 | if err := findRedditError(data); err != nil { 60 | return nil, err 61 | } 62 | 63 | auth := Reddit{} 64 | auth.Chain = make(chan *ChainVals, 32) 65 | json.Unmarshal(data, &auth) 66 | auth.Creds = *c 67 | return &auth, nil 68 | } 69 | 70 | // This goroutine reauthenticates the user 71 | // every 45 minutes. It should be run with the go 72 | // statement 73 | func (c *Reddit) autoRefresh() { 74 | for { 75 | time.Sleep(45 * time.Minute) 76 | c.updateCredentials() 77 | } 78 | } 79 | 80 | // Reauthenticate and updates the object itself 81 | func (c *Reddit) updateCredentials() { 82 | temp, _ := Authenticate(&c.Creds) 83 | // Just updated the token 84 | c.Token = temp.Token 85 | } 86 | 87 | // SetDefault sets all default values 88 | func (c *Reddit) SetDefault() { 89 | c.Stream = Streaming{ 90 | CommentListInterval: 8, 91 | PostListInterval: 10, 92 | ReportsInterval: 15, 93 | ModQueueInterval: 15, 94 | PostListSlice: 25, 95 | } 96 | c.Values = RedditVals{ 97 | GetSubmissionFromCommentTries: 32, 98 | } 99 | } 100 | 101 | // SetClient sets mira's *http.Client to make requests 102 | func (c *Reddit) SetClient(client *http.Client) { 103 | c.Client = client 104 | } 105 | -------------------------------------------------------------------------------- /comments.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | 10 | "github.com/thecsw/mira/v4/models" 11 | ) 12 | 13 | // Comments returns comments from a subreddit up to a specified limit sorted by the given parameters 14 | // 15 | // Sorting options: `Hot`, `New`, `Top`, `Rising`, `Controversial`, `Random` 16 | // 17 | // Duration options: `Hour`, `Day`, `Week`, `Year`, `All` 18 | // 19 | // Limit is any numerical value, so 0 <= limit <= 100. 20 | func (c *Reddit) Comments(sort string, tdur string, limit int) ([]models.Comment, error) { 21 | name, ttype := c.getQueue() 22 | switch ttype { 23 | case subredditType: 24 | return c.getSubredditComments(name, sort, tdur, limit) 25 | case submissionType: 26 | comments, _, err := c.getSubmissionComments(name, sort, tdur, limit) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return comments, nil 31 | case redditorType: 32 | return c.getRedditorComments(name, sort, tdur, limit) 33 | default: 34 | return nil, fmt.Errorf("'%s' type does not have an option for comments", ttype) 35 | } 36 | } 37 | 38 | // CommentsAfter returns new comments from a subreddit 39 | // 40 | // # Last is the anchor of a comment id 41 | // 42 | // Limit is any numerical value, so 0 <= limit <= 100. 43 | func (c *Reddit) CommentsAfter(sort string, last string, limit int) ([]models.Comment, error) { 44 | name, ttype := c.getQueue() 45 | switch ttype { 46 | case subredditType: 47 | return c.getSubredditCommentsAfter(name, sort, last, limit) 48 | case redditorType: 49 | return c.getRedditorCommentsAfter(name, sort, last, limit) 50 | default: 51 | return nil, fmt.Errorf("'%s' type does not have an option for comments", ttype) 52 | } 53 | } 54 | 55 | func (c *Reddit) getComment(id string) (models.Comment, error) { 56 | target := RedditOauth + "/api/info.json" 57 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 58 | "id": id, 59 | }) 60 | ret := &models.CommentListing{} 61 | json.Unmarshal(ans, ret) 62 | if len(ret.GetChildren()) < 1 { 63 | return models.Comment{}, fmt.Errorf("id not found") 64 | } 65 | return ret.GetChildren()[0], err 66 | } 67 | 68 | func (c *Reddit) getSubredditComments(sr string, sort string, tdur string, limit int) ([]models.Comment, error) { 69 | target := RedditOauth + "/r/" + sr + "/comments.json" 70 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 71 | "sort": sort, 72 | "limit": strconv.Itoa(limit), 73 | "t": tdur, 74 | }) 75 | ret := &models.CommentListing{} 76 | json.Unmarshal(ans, ret) 77 | return ret.GetChildren(), err 78 | } 79 | 80 | func (c *Reddit) getSubredditCommentsAfter(sr string, sort string, last string, limit int) ([]models.Comment, error) { 81 | target := RedditOauth + "/r/" + sr + "/comments.json" 82 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 83 | "sort": sort, 84 | "limit": strconv.Itoa(limit), 85 | "before": last, 86 | }) 87 | ret := &models.CommentListing{} 88 | json.Unmarshal(ans, ret) 89 | return ret.GetChildren(), err 90 | } 91 | 92 | func (c *Reddit) getRedditorComments(user string, sort string, tdur string, limit int) ([]models.Comment, error) { 93 | target := RedditOauth + "/u/" + user + "/comments.json" 94 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 95 | "sort": sort, 96 | "limit": strconv.Itoa(limit), 97 | "t": tdur, 98 | }) 99 | ret := &models.CommentListing{} 100 | json.Unmarshal(ans, ret) 101 | return ret.GetChildren(), err 102 | } 103 | 104 | func (c *Reddit) getRedditorCommentsAfter(user string, sort string, last string, limit int) ([]models.Comment, error) { 105 | target := RedditOauth + "/u/" + user + "/comments.json" 106 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 107 | "sort": sort, 108 | "limit": strconv.Itoa(limit), 109 | "before": last, 110 | }) 111 | ret := &models.CommentListing{} 112 | json.Unmarshal(ans, ret) 113 | return ret.GetChildren(), err 114 | } 115 | 116 | func (c *Reddit) getSubmissionComments(postID string, sort string, tdur string, limit int) ([]models.Comment, []string, error) { 117 | if string(postID[1]) != "3" { 118 | return nil, nil, errors.New("the passed ID36 is not a submission") 119 | } 120 | target := RedditOauth + "/comments/" + postID[3:] 121 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 122 | "sort": sort, 123 | "limit": strconv.Itoa(limit), 124 | "showmore": strconv.FormatBool(true), 125 | "t": tdur, 126 | }) 127 | if err != nil { 128 | return nil, nil, err 129 | } 130 | temp := make([]models.CommentListing, 0, 8) 131 | json.Unmarshal(ans, &temp) 132 | ret := make([]models.Comment, 0, 8) 133 | for _, v := range temp { 134 | comments := v.GetChildren() 135 | ret = append(ret, comments...) 136 | } 137 | // Cut off the "more" kind 138 | children := ret[len(ret)-1].Data.Children 139 | ret = ret[:len(ret)-1] 140 | return ret, children, nil 141 | } 142 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | const ( 4 | // RedditBase is the basic reddit base URL, authed is the base URL for use once authenticated 5 | RedditBase = "https://www.reddit.com/" 6 | // RedditOauth is the oauth url to pass the tokens to 7 | RedditOauth = "https://oauth.reddit.com" 8 | 9 | // JsonAPI sets the api type to json 10 | JsonAPI = "json" 11 | 12 | // Sorting options 13 | Hot = "hot" 14 | New = "new" 15 | Top = "top" 16 | Rising = "rising" 17 | Controversial = "controversial" 18 | Random = "random" 19 | 20 | // Duration options 21 | Hour = "hour" 22 | Day = "day" 23 | Week = "week" 24 | Year = "year" 25 | All = "all" 26 | ) 27 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package mira is fantastic reddit api wrapper 2 | // 3 | // README at https://github.com/thecsw/mira/v4 4 | // 5 | // All function docs here 6 | package mira 7 | -------------------------------------------------------------------------------- /examples/comments/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/thecsw/mira/v4" 7 | ) 8 | 9 | // Errors are omitted for brevity 10 | func main() { 11 | r, _ := mira.Init(mira.ReadCredsFromFile("login.conf")) 12 | 13 | // Make a submission 14 | post, _ := r.Subreddit("mysubreddit").Submit("mytitle", "mytext") 15 | 16 | // Comment on our new submission 17 | comment, _ := r.Submission(post.GetId()).Save("mycomment") 18 | 19 | // Reply to our own comment 20 | reply, _ := r.Comment(comment.GetId()).Reply("myreply") 21 | 22 | // Delete the reply 23 | r.Comment(reply.GetId()).Delete() 24 | 25 | // Edit the first comment 26 | newComment, _ := r.Comment(comment.GetId()).Edit("myedit") 27 | 28 | // Show the comment's body 29 | fmt.Println(newComment.GetBody()) 30 | } 31 | -------------------------------------------------------------------------------- /examples/composing/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/thecsw/mira/v4" 5 | ) 6 | 7 | func main() { 8 | r, _ := mira.Init(mira.ReadCredsFromFile("login.conf")) 9 | 10 | r.Redditor("myuser").Compose("mytitle", "mytext") 11 | } 12 | -------------------------------------------------------------------------------- /examples/demo/reddit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | 6 | "github.com/thecsw/mira/v4" 7 | ) 8 | 9 | func main() { 10 | r, err := mira.Init(mira.ReadCredsFromFile("login.conf")) 11 | check(err) 12 | log.Infoln("Connected to reddit!") 13 | 14 | me, err := r.Me().Info() 15 | check(err) 16 | log.Infoln("My reddit name is", me.GetAuthor()) 17 | 18 | log.Infoln("Let's listen to some hot r/all stuff!") 19 | posts, err := r.Subreddit("all").Submissions("hot", "day", 5) 20 | check(err) 21 | for i, v := range posts { 22 | log.Infof("%d | %s by %s (%0.f upvotes)", 23 | i, v.GetId(), v.GetAuthor(), v.GetUps()) 24 | } 25 | 26 | log.Infoln("That's it!") 27 | } 28 | 29 | func check(err error) { 30 | if err != nil { 31 | log.Panic(err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/stream_comment_replies/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/thecsw/mira/v4" 5 | ) 6 | 7 | func main() { 8 | // Good practice is to check if the login errors out or not 9 | r, _ := mira.Init(mira.ReadCredsFromFile("login.conf")) 10 | c := r.StreamCommentReplies() 11 | for { 12 | msg := <-c 13 | r.Comment(msg.GetId()).Reply("I got your message!") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/stream_comments/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/thecsw/mira/v4" 5 | ) 6 | 7 | func main() { 8 | r, _ := mira.Init(mira.ReadCredsFromFile("login.conf")) 9 | c, _ := r.Subreddit("all").StreamComments() 10 | for { 11 | msg := <-c 12 | r.Comment(msg.GetId()).Reply("myreply") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/stream_submissions/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/thecsw/mira/v4" 5 | ) 6 | 7 | func main() { 8 | r, _ := mira.Init(mira.ReadCredsFromFile("login.conf")) 9 | c, _ := r.Subreddit("all").StreamSubmissions() 10 | for { 11 | post := <-c 12 | r.Submission(post.GetId()).Save("hello there") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/submissions/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/thecsw/mira/v4" 7 | ) 8 | 9 | func main() { 10 | r, _ := mira.Init(mira.ReadCredsFromFile("login.conf")) 11 | sort := "top" 12 | var limit = 25 13 | duration := "all" 14 | subs, _ := r.Subreddit("all").Submissions(sort, duration, limit) 15 | for _, v := range subs { 16 | fmt.Println("Submission Title: ", v.GetTitle()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thecsw/mira/v4 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/pkg/errors v0.9.1 7 | github.com/sirupsen/logrus v1.9.3 8 | ) 9 | 10 | require golang.org/x/sys v0.22.0 // indirect 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 5 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 9 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 12 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 14 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 15 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /messages.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/thecsw/mira/v4/models" 8 | ) 9 | 10 | // Compose will send a message to next redditor. 11 | func (c *Reddit) Compose(subject, text string) error { 12 | name, _, err := c.checkType(redditorType) 13 | if err != nil { 14 | return err 15 | } 16 | target := RedditOauth + "/api/compose" 17 | _, err = c.MiraRequest(http.MethodPost, target, map[string]string{ 18 | "subject": subject, 19 | "text": text, 20 | "to": name, 21 | "api_type": JsonAPI, 22 | }) 23 | return err 24 | } 25 | 26 | // ReadMessage marks the next comment/message as read. 27 | func (c *Reddit) ReadMessage(messageID string) error { 28 | _, _, err := c.checkType(meType) 29 | if err != nil { 30 | return err 31 | } 32 | target := RedditOauth + "/api/read_message" 33 | _, err = c.MiraRequest(http.MethodPost, target, map[string]string{ 34 | "id": messageID, 35 | }) 36 | return err 37 | } 38 | 39 | // ReadAllMessages uses ReadMessage on all unread messages. 40 | func (c *Reddit) ReadAllMessages() error { 41 | _, _, err := c.checkType(meType) 42 | if err != nil { 43 | return err 44 | } 45 | target := RedditOauth + "/api/read_all_messages" 46 | _, err = c.MiraRequest(http.MethodPost, target, nil) 47 | return err 48 | } 49 | 50 | // ListUnreadMessages returns a list of all unread messages. 51 | func (c *Reddit) ListUnreadMessages() ([]models.Comment, error) { 52 | _, _, err := c.checkType(meType) 53 | if err != nil { 54 | return nil, err 55 | } 56 | target := RedditOauth + "/message/unread" 57 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 58 | "mark": "false", 59 | }) 60 | ret := &models.CommentListing{} 61 | json.Unmarshal(ans, ret) 62 | return ret.GetChildren(), err 63 | } 64 | -------------------------------------------------------------------------------- /mira.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import "net/http" 4 | 5 | // Init is used 6 | // when we initialize the Reddit instance, 7 | // automatically start a goroutine that will 8 | // update the token every 45 minutes. The 9 | // auto_refresh should not be accessible to 10 | // the end user as it is an internal method 11 | func Init(c Credentials) (*Reddit, error) { 12 | auth, err := Authenticate(&c) 13 | if err != nil { 14 | return nil, err 15 | } 16 | auth.Client = &http.Client{} 17 | auth.SetDefault() 18 | go auth.autoRefresh() 19 | return auth, nil 20 | } 21 | -------------------------------------------------------------------------------- /mira.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecsw/mira/1301b29dd6d41d1c5f976fecc642c4c1a848cf8f/mira.png -------------------------------------------------------------------------------- /mod.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | ) 7 | 8 | // Approve is a mod tool to approve a comment or a submission 9 | // Will fail if not a mod. 10 | func (c *Reddit) Approve() error { 11 | name, _, err := c.checkType(commentType) 12 | if err != nil { 13 | return err 14 | } 15 | target := RedditOauth + "/api/approve" 16 | _, err = c.MiraRequest(http.MethodPost, target, map[string]string{ 17 | "id": name, 18 | "api_type": JsonAPI, 19 | }) 20 | return err 21 | } 22 | 23 | // Distinguish is a mod tool to distinguish a comment or a submission 24 | // Will fail if not a mod. 25 | func (c *Reddit) Distinguish(how string, sticky bool) error { 26 | name, _, err := c.checkType(commentType) 27 | if err != nil { 28 | return err 29 | } 30 | target := RedditOauth + "/api/distinguish" 31 | _, err = c.MiraRequest(http.MethodPost, target, map[string]string{ 32 | "id": name, 33 | "how": how, 34 | "sticky": strconv.FormatBool(sticky), 35 | "api_type": JsonAPI, 36 | }) 37 | return err 38 | } 39 | 40 | // UpdateSidebar updates subreddit's sidebar, Needs mod privileges. 41 | func (c *Reddit) UpdateSidebar(text string) error { 42 | name, _, err := c.checkType(subredditType) 43 | if err != nil { 44 | return err 45 | } 46 | target := RedditOauth + "/api/site_admin" 47 | _, err = c.MiraRequest(http.MethodPost, target, map[string]string{ 48 | "sr": name, 49 | "name": "None", 50 | "description": text, 51 | "title": name, 52 | "wikimode": "anyone", 53 | "link_type": "any", 54 | "type": "public", 55 | "api_type": JsonAPI, 56 | }) 57 | return err 58 | } 59 | 60 | // SelectFlair sets a submission flair. 61 | func (c *Reddit) SelectFlair(text string) error { 62 | name, _, err := c.checkType(submissionType) 63 | if err != nil { 64 | return err 65 | } 66 | target := RedditOauth + "/api/selectflair" 67 | _, err = c.MiraRequest(http.MethodPost, target, map[string]string{ 68 | "link": name, 69 | "text": text, 70 | "api_type": JsonAPI, 71 | }) 72 | return err 73 | } 74 | 75 | // SelectFlairWithID sets submission flair with explicit ID. 76 | func (c *Reddit) SelectFlairWithID(name, text string) error { 77 | target := RedditOauth + "/api/selectflair" 78 | _, err := c.MiraRequest(http.MethodPost, target, map[string]string{ 79 | "link": name, 80 | "text": text, 81 | "api_type": JsonAPI, 82 | }) 83 | return err 84 | } 85 | 86 | // UserFlair updates user's flair in a sub. Needs mod permissions. 87 | func (c *Reddit) UserFlair(user, text string) error { 88 | name, _, err := c.checkType(subredditType) 89 | if err != nil { 90 | return err 91 | } 92 | target := RedditOauth + "/r/" + name + "/api/flair" 93 | _, err = c.MiraRequest(http.MethodPost, target, map[string]string{ 94 | "name": user, 95 | "text": text, 96 | "api_type": JsonAPI, 97 | }) 98 | return err 99 | } 100 | 101 | // UserFlairWithID is the same as UserFlair but explicit redditor name. 102 | func (c *Reddit) UserFlairWithID(name, user, text string) error { 103 | target := RedditOauth + "/r/" + name + "/api/flair" 104 | _, err := c.MiraRequest(http.MethodPost, target, map[string]string{ 105 | "name": user, 106 | "text": text, 107 | "api_type": JsonAPI, 108 | }) 109 | return err 110 | } 111 | -------------------------------------------------------------------------------- /models/comment.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | func (c CommentWrap) getThing() CommentJsonDataThing { 4 | if len(c.Json.Data.Things) > 0 { 5 | return c.Json.Data.Things[0] 6 | } 7 | return CommentJsonDataThing{} 8 | } 9 | func (c CommentWrap) GetId() string { return c.getThing().Data.Name } 10 | func (c CommentWrap) GetSubredditId() string { return c.getThing().Data.SubredditId } 11 | func (c CommentWrap) GetParentId() string { return c.getThing().Data.ParentId } 12 | func (c CommentWrap) GetAuthor() string { return c.getThing().Data.Author } 13 | func (c CommentWrap) GetAuthorId() string { return c.getThing().Data.AuthorFullname } 14 | func (c CommentWrap) GetSubreddit() string { return c.getThing().Data.Subreddit } 15 | func (c CommentWrap) CreatedAt() float64 { return c.getThing().Data.Created } 16 | func (c CommentWrap) GetBody() string { return c.getThing().Data.Body } 17 | func (c CommentWrap) GetScore() float64 { return c.getThing().Data.Score } 18 | func (c CommentWrap) GetUps() float64 { return c.getThing().Data.Ups } 19 | func (c CommentWrap) GetDowns() float64 { return c.getThing().Data.Downs } 20 | func (c CommentWrap) IsSticky() bool { return c.getThing().Data.Stickied } 21 | func (c CommentWrap) IsRemoved() bool { return c.getThing().Data.Removed } 22 | func (c CommentWrap) IsApproved() bool { return c.getThing().Data.Approved } 23 | func (c CommentWrap) IsAuthor() bool { return c.getThing().Data.IsSubmitter } 24 | -------------------------------------------------------------------------------- /models/comment_struct.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type CommentWrap struct { 4 | Json CommentJson `json:"json"` 5 | } 6 | 7 | type CommentJson struct { 8 | Errors []string `json:"errors"` 9 | Data CommentJsonData `json:"data"` 10 | } 11 | 12 | type CommentJsonData struct { 13 | Things []CommentJsonDataThing `json:"things"` 14 | } 15 | 16 | type CommentJsonDataThing struct { 17 | Kind string `json:"kind"` 18 | Data CommentJsonDataThingData `json:"data"` 19 | } 20 | 21 | type CommentJsonDataThingData struct { 22 | AuthorFlairBackgroundColor string `json:"author_flair_background_color"` 23 | TotalAwardsReceived float64 `json:"total_awards_received"` 24 | ApprovedAtUtc string `json:"approved_at_utc"` 25 | Distinguished string `json:"distinguished"` 26 | ModReasonBy string `json:"mod_reason_by"` 27 | BannedBy string `json:"banned_by"` 28 | AuthorFlairType string `json:"author_flair_type"` 29 | RemovalReason string `json:"removal_reason"` 30 | LinkId string `json:"link_id"` 31 | AuthorFlairTemplateId string `json:"author_flair_template_id"` 32 | Likes bool `json:"likes"` 33 | Replies string `json:"replies"` 34 | UserReports []string `json:"user_reports"` 35 | Saved bool `json:"saved"` 36 | Id string `json:"id"` 37 | BannedAtUtc string `json:"banned_at_utc"` 38 | ModReasonTitle string `json:"mod_reason_title"` 39 | Gilded float64 `json:"gilded"` 40 | Archived bool `json:"archived"` 41 | NoFollow bool `json:"no_follow"` 42 | Author string `json:"author"` 43 | RteMode string `json:"rte_mode"` 44 | CanModPost bool `json:"can_mod_post"` 45 | CreatedUtc float64 `json:"created_utc"` 46 | SendReplies bool `json:"send_replies"` 47 | ParentId string `json:"parent_id"` 48 | Score float64 `json:"score"` 49 | AuthorFullname string `json:"author_fullname"` 50 | ApprovedBy string `json:"approved_by"` 51 | Mod_note string `json:"mod_note"` 52 | AllAwardings []string `json:"all_awardings"` 53 | SubredditId string `json:"subreddit_id"` 54 | Body string `json:"body"` 55 | Edited bool `json:"edited"` 56 | Gildings Gilding `json:"gildings"` 57 | AuthorFlairCssClass string `json:"author_flair_css_class"` 58 | Name string `json:"name"` 59 | AuthorPatreonFlair bool `json:"author_patreon_flair"` 60 | Downs float64 `json:"downs"` 61 | AuthorFlairRichtext []string `json:"author_flair_richtext"` 62 | IsSubmitter bool `json:"is_submitter"` 63 | CollapsedReason string `json:"collapsed_reason"` 64 | BodyHtml string `json:"body_html"` 65 | Stickied bool `json:"stickied"` 66 | CanGild bool `json:"can_gild"` 67 | Removed bool `json:"removed"` 68 | Approved bool `json:"approved"` 69 | AuthorFlairTextColor string `json:"author_flair_text_color"` 70 | ScoreHidden bool `json:"score_hidden"` 71 | Permalink string `json:"permalink"` 72 | NumReports float64 `json:"num_reports"` 73 | Locked bool `json:"locked"` 74 | ReportReasons []string `json:"report_reasons"` 75 | Created float64 `json:"created"` 76 | Subreddit string `json:"subreddit"` 77 | AuthorFlairText string `json:"author_flair_text"` 78 | Spam bool `json:"spam"` 79 | Collapsed bool `json:"collapsed"` 80 | SubredditNamePrefixed string `json:"subreddit_name_prefixed"` 81 | Controversiality float64 `json:"controversiality"` 82 | IgnoreReports bool `json:"ignore_reports"` 83 | ModReports []string `json:"mod_reports"` 84 | SubredditType string `json:"subreddit_type"` 85 | Ups float64 `json:"ups"` 86 | } 87 | 88 | type Gilding struct { 89 | Gid map[string]int `json:"gid"` 90 | } 91 | -------------------------------------------------------------------------------- /models/comment_struct_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkCreateComment(b *testing.B) { 10 | data, _ := ioutil.ReadFile("./tests/comment.json") 11 | commentExampleJson := string(data) 12 | for i := 0; i < b.N; i++ { 13 | sub := CommentWrap{} 14 | json.Unmarshal([]byte(commentExampleJson), &sub) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/commentlisting.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | func (l *CommentListing) GetChildren() []Comment { return l.Data.Children } 4 | func (ldc Comment) GetId() string { return ldc.Data.Name } 5 | func (ldc Comment) GetParentId() string { return ldc.Data.ParentId } 6 | func (ldc Comment) IsRoot() bool { return string(ldc.Data.ParentId[1]) == "3" } 7 | func (ldc Comment) GetTitle() string { return ldc.Data.Body } 8 | func (ldc Comment) GetBody() string { return ldc.Data.Body } 9 | func (ldc Comment) GetSubreddit() string { return ldc.Data.Subreddit } 10 | func (ldc Comment) GetUps() float64 { return ldc.Data.Score } 11 | func (ldc Comment) GetDowns() float64 { return 0 } 12 | func (ldc Comment) GetCreated() float64 { return ldc.Data.Created } 13 | func (ldc Comment) GetAuthor() string { return ldc.Data.Author } 14 | func (ldc Comment) IsComment() bool { return ldc.Kind == "t1" } 15 | func (ldc Comment) IsCommentReply() bool { return ldc.Data.Subject == "comment reply" } 16 | func (ldc Comment) IsMention() bool { return ldc.Data.Subject == "username mention" } 17 | func (ldc Comment) GetName() string { return ldc.Data.Name } 18 | func (ldc Comment) GetKarma() float64 { return ldc.Data.Score } 19 | func (ldc Comment) GetUrl() string { return ldc.Data.Permalink } 20 | func (ldc Comment) GetFlair() string { return ldc.Data.Context } 21 | -------------------------------------------------------------------------------- /models/commentlisting_struct.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type CommentListing struct { 4 | Kind string `json:"kind"` 5 | Data CommentListingData `json:"data"` 6 | } 7 | 8 | type CommentListingData struct { 9 | Modhash string `json:"modhash"` 10 | Dist float64 `json:"dist"` 11 | Children []Comment `json:"children"` 12 | After string `json:"after"` 13 | Before string `json:"before"` 14 | } 15 | 16 | type Comment struct { 17 | Kind string `json:"kind"` 18 | Data CommentListingDataChildrenData `json:"data"` 19 | } 20 | 21 | type CommentListingDataChildrenData struct { 22 | FirstMessage string `json:"first_message"` 23 | FirstMessageName string `json:"first_message_name"` 24 | Subreddit string `json:"subreddit"` 25 | Likes string `json:"likes"` 26 | Replies string `json:"replies"` 27 | Id string `json:"id"` 28 | Subject string `json:"subject"` 29 | WasComment bool `json:"was_comment"` 30 | Score float64 `json:"score"` 31 | Author string `json:"author"` 32 | NumComments float64 `json:"num_comments"` 33 | ParentId string `json:"parent_id"` 34 | SubredditNamePrefixed string `json:"subreddit_name_prefixed"` 35 | New bool `json:"new"` 36 | Body string `json:"body"` 37 | LinkTitle string `json:"link_title"` 38 | LinkId string `json:"link_id"` 39 | Permalink string `json:"permalink"` 40 | Dest string `json:"dest"` 41 | BodyHtml string `json:"body_html"` 42 | Name string `json:"name"` 43 | Created float64 `json:"created"` 44 | Created_utc float64 `json:"created_utc"` 45 | Context string `json:"context"` 46 | Distinguished string `json:"distinguished"` 47 | Children []string `json:"children"` 48 | ModReasonBy string `json:"mod_reason_by"` 49 | RemovalReason string `json:"removal_reason"` 50 | BannedBy string `json:"banned_by"` 51 | BannedAtUtc float64 `json:"banned_at_utc"` 52 | UserReports []interface{} `json:"user_reports"` 53 | } 54 | -------------------------------------------------------------------------------- /models/commentlisting_struct_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkCreateCommentListing(b *testing.B) { 10 | data, _ := ioutil.ReadFile("./tests/commentlisting.json") 11 | commentListingExampleJson := string(data) 12 | for i := 0; i < b.N; i++ { 13 | sub := CommentListing{} 14 | json.Unmarshal([]byte(commentListingExampleJson), &sub) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/me.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | func (me Me) GetId() string { return me.Id } 4 | func (me Me) GetName() string { return me.Name } 5 | func (me Me) GetAuthor() string { return me.Name } 6 | func (me Me) GetParentId() string { return me.Id } 7 | func (me Me) GetTitle() string { return me.Name } 8 | func (me Me) GetBody() string { return me.Name } 9 | func (me Me) GetKarma() float64 { return me.CommentKarma + me.LinkKarma } 10 | func (me Me) GetUps() float64 { return 0 } 11 | func (me Me) GetDowns() float64 { return 0 } 12 | func (me Me) GetSubreddit() string { return me.Name } 13 | func (me Me) GetCreated() float64 { return me.Created } 14 | func (me Me) GetFlair() string { return "" } 15 | func (me Me) GetUrl() string { return me.IconImg } 16 | func (me Me) IsRoot() bool { return true } 17 | -------------------------------------------------------------------------------- /models/me_struct.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Me struct { 4 | IsEmployee bool `json:"is_employee"` 5 | SeenLayoutSwitch bool `json:"seen_layout_switch"` 6 | HasVisitedNewProfile bool `json:"has_visited_new_profile"` 7 | PrefNoProfanity bool `json:"pref_no_profanity"` 8 | HasExternalAccount bool `json:"has_external_account"` 9 | PrefGeopopular string `json:"pref_geopopular"` 10 | SeenRedesignModal bool `json:"seen_redesign_modal"` 11 | PrefShowTrending bool `json:"pref_show_trending"` 12 | Subreddit Subreddit_s `json:"subreddit"` 13 | IsSponsor bool `json:"is_sponsor"` 14 | GoldExpiration float64 `json:"gold_expiration"` 15 | HasGoldSubscription bool `json:"has_gold_subscription"` 16 | NumFriends float64 `json:"num_friends"` 17 | Features MeFeatures `json:"features"` 18 | HasAndroidSubscription bool `json:"has_android_subscription"` 19 | Verified bool `json:"verified"` 20 | NewModmailExists bool `json:"new_modmail_exists"` 21 | PrefAutoplay bool `json:"pref_autoplay"` 22 | Coins float64 `json:"coins"` 23 | HasPaypalSubscription bool `json:"has_paypal_subscription"` 24 | HasSubscribedToPremium bool `json:"has_subscribed_to_premium"` 25 | Id string `json:"id"` 26 | HasStripeSubscription bool `json:"has_stripe_subscription"` 27 | SeenPremiumAdblockModal bool `json:"seen_premium_adblock_modal"` 28 | CanCreateSubreddit bool `json:"can_create_subreddit"` 29 | Over18 bool `json:"over_18"` 30 | IsGold bool `json:"is_gold"` 31 | IsMod bool `json:"is_mod"` 32 | SuspensionExpirationUtc float64 `json:"suspension_expiration_utc"` 33 | HasVerifiedEmail bool `json:"has_verified_email"` 34 | IsSuspended bool `json:"is_suspended"` 35 | PrefVideoAutoplay bool `json:"pref_video_autoplay"` 36 | InChat bool `json:"in_chat"` 37 | InRedesignBeta bool `json:"in_redesign_beta"` 38 | IconImg string `json:"icon_img"` 39 | HasModMail bool `json:"has_mod_mail"` 40 | PrefNightmode bool `json:"pref_nightmode"` 41 | OauthClientId bool `json:"oauth_client_id"` 42 | HideFromRobots bool `json:"hide_from_robots"` 43 | LinkKarma float64 `json:"link_karma"` 44 | ForcePasswordReset bool `json:"force_password_reset"` 45 | InboxCount float64 `json:"inbox_count"` 46 | PrefTopKarmaSubreddits bool `json:"pref_top_karma_subreddits"` 47 | HasMail bool `json:"has_mail"` 48 | PrefShowSnoovatar bool `json:"pref_show_snoovatar"` 49 | Name string `json:"name"` 50 | PrefClickgadget float64 `json:"pref_clickgadget"` 51 | Created float64 `json:"created"` 52 | GoldCreddits float64 `json:"gold_creddits"` 53 | HasIosSubscription bool `json:"has_ios_subscription"` 54 | PrefShowTwitter bool `json:"pref_show_twitter"` 55 | InBeta bool `json:"in_beta"` 56 | CommentKarma float64 `json:"comment_karma"` 57 | HasSubscribed bool `json:"has_subscribed"` 58 | SeenSubredditChatFtux bool `json:"seen_subreddit_chat_ftux"` 59 | } 60 | 61 | type MeFeatures struct { 62 | RichtextPreviews bool `json:"richtext_previews"` 63 | DoNotTrack bool `json:"do_not_track"` 64 | ChatSubreddit bool `json:"chat_subreddit"` 65 | Chat bool `json:"chat"` 66 | SeqRandomizeSort bool `json:"seq_randomize_sort"` 67 | Sequence bool `json:"sequence"` 68 | MwebXpromoRevampV2 MeSubFeature `json:"mweb_xpromo_revamp_v2"` 69 | MwebXpromoFloat64erstitialCommentsIos bool `json:"mweb_xpromo_float64erstitial_comments_ios"` 70 | ChatReddarReports bool `json:"chat_reddar_reports"` 71 | ChatRollout bool `json:"chat_rollout"` 72 | MwebXpromoFloat64erstitialCommentsAndroid bool `json:"mwev_xpromo_float64erstitial_comments_android"` 73 | ChatGroutRollout bool `json:"chat_group_rollout"` 74 | MwebLinkTab MeSubFeature `json:"mweb_link_tab"` 75 | SpezModal bool `json:"spez_modal"` 76 | CommunityAwards bool `json:"community_awards"` 77 | DefaultSrsHoldout MeSubFeature `json:"default_srs_holdout"` 78 | ChatUserSettings bool `json:"chat_user_settings"` 79 | DualWriteUserPrefs bool `json:"dual_write_user_prefs"` 80 | 81 | MwebXpromoModalListingClickDailyDismissibleAndroid bool `json:"mweb_xpromo_modal_listing_click_daily_dismissible_ios"` 82 | MwebXpromoModalListingClickDailyDismisssibleIos bool `json:"mweb_xpromo_modal_listing_click_daily_dismissible_android"` 83 | } 84 | 85 | type MeSubFeature struct { 86 | Owner string `json:"owner"` 87 | Variant string `json:"variant"` 88 | ExperimentId float64 `json:"experiment_id"` 89 | } 90 | -------------------------------------------------------------------------------- /models/me_struct_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkCreateMe(b *testing.B) { 10 | data, _ := ioutil.ReadFile("./tests/me.json") 11 | meExampleJson := string(data) 12 | for i := 0; i < b.N; i++ { 13 | sub := Me{} 14 | json.Unmarshal([]byte(meExampleJson), &sub) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/modqueue.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | func (mql ModQueueListingChild) GetKind() string { return mql.Kind } 4 | func (mql ModQueueListingChild) GetId() string { 5 | return mql.Data.Name 6 | } 7 | func (mql *ModQueueListing) GetChildren() []ModQueueListingChild { return mql.Data.Children } 8 | -------------------------------------------------------------------------------- /models/modqueue_struct.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ModQueueListing struct { 4 | Kind string `json:"kind"` 5 | Data ModQueueListingData `json:"data"` 6 | } 7 | 8 | type ModQueueListingData struct { 9 | Modhash string `json:"modhash"` 10 | Dist float64 `json:"dist"` 11 | Children []ModQueueListingChild `json:"children"` 12 | } 13 | 14 | type ModQueueListingChild struct { 15 | Kind string `json:"kind"` 16 | Data PostListingChildData `json:"data"` 17 | } 18 | -------------------------------------------------------------------------------- /models/modqueue_struct_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkCreateModQueueListing(b *testing.B) { 10 | data, _ := ioutil.ReadFile("./tests/modqueue.json") 11 | modQueueListingExampleJson := string(data) 12 | for i := 0; i < b.N; i++ { 13 | sub := ModQueueListing{} 14 | json.Unmarshal([]byte(modQueueListingExampleJson), &sub) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/modqueue_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func TestGetModQueueListingId(t *testing.T) { 10 | modqueue := ModQueueListing{} 11 | data, _ := ioutil.ReadFile("./tests/modqueue.json") 12 | json.Unmarshal(data, &modqueue) 13 | if v := modqueue.GetChildren()[0].GetId(); v != `t1_hw8ecqj` { 14 | t.Error( 15 | "For GetId()", 16 | "expected", `t1_hw8ecqj`, 17 | "got", v, 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /models/post.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | func (p *PostListing) GetChildren() []PostListingChild { return p.Data.Children } 4 | func (plc PostListingChild) GetSubreddit() string { return plc.Data.Subreddit } 5 | func (plc PostListingChild) GetSubredditId() string { return plc.Data.SubredditId } 6 | func (plc PostListingChild) GetName() string { return plc.Data.Title } 7 | func (plc PostListingChild) GetTitle() string { return plc.Data.Title } 8 | func (plc PostListingChild) GetId() string { return plc.Data.Name } 9 | func (plc PostListingChild) GetParentId() string { return plc.Data.Name } 10 | func (plc PostListingChild) GetAuthor() string { return plc.Data.Author } 11 | func (plc PostListingChild) GetAuthorId() string { return plc.Data.AuthorFullname } 12 | func (plc PostListingChild) GetCreated() float64 { return plc.Data.Created } 13 | func (plc PostListingChild) GetKarma() float64 { return plc.Data.Ups - plc.Data.Downs } 14 | func (plc PostListingChild) GetUps() float64 { return plc.Data.Ups } 15 | func (plc PostListingChild) GetDowns() float64 { return plc.Data.Downs } 16 | func (plc PostListingChild) GetScore() float64 { return plc.Data.Score } 17 | func (plc PostListingChild) GetBody() string { return plc.Data.Selftext } 18 | func (plc PostListingChild) GetAuthorFlair() string { return plc.Data.AuthorFlairText } 19 | func (plc PostListingChild) GetPermalink() string { return plc.Data.Permalink } 20 | func (plc PostListingChild) GetUrl() string { return plc.Data.Url } 21 | func (plc PostListingChild) GetFlair() string { return plc.Data.LinkFlairText } 22 | func (plc PostListingChild) GetCommentCount() float64 { return plc.Data.NumComments } 23 | func (plc PostListingChild) GetCrosspostCount() float64 { return plc.Data.NumCrossposts } 24 | func (plc PostListingChild) IsRoot() bool { return true } 25 | -------------------------------------------------------------------------------- /models/post_struct.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type PostListing struct { 4 | Kind string `json:"kind"` 5 | Data PostListingData `json:"data"` 6 | } 7 | 8 | type PostListingData struct { 9 | Modhash string `json:"modhash"` 10 | Dist float64 `json:"dist"` 11 | Children []PostListingChild `json:"children"` 12 | } 13 | 14 | type PostListingChild struct { 15 | Kind string `json:"kind"` 16 | Data PostListingChildData `json:"data"` 17 | After string `json:"after"` 18 | Before string `json:"before"` 19 | } 20 | 21 | type PostListingChildData struct { 22 | ApprovedAtUtc float64 `json:"approved_at_utc"` 23 | Subreddit string `json:"subreddit"` 24 | Selftext string `json:"selftext"` 25 | AuthorFullname string `json:"author_fullname"` 26 | Saved bool `json:"saved"` 27 | ModReasonTitle string `json:"mod_reason_title"` 28 | Gilded float64 `json:"gilded"` 29 | Clicked bool `json:"clicked"` 30 | Title string `json:"title"` 31 | LinkFlairRichtext string `json:"link_flair_richtext"` 32 | SubredditNamePrefixed string `json:"subreddit_name_prefixed"` 33 | Hidden bool `json:"hidden"` 34 | Pwls float64 `json:"pwls"` 35 | LinkFlairCssClass string `json:"link_flair_css_class"` 36 | Downs float64 `json:"downs"` 37 | ThumbnailHeight float64 `json:"thumbnail_height"` 38 | HideScore bool `json:"hide_score"` 39 | Name string `json:"name"` 40 | Quarantine bool `json:"quarantine"` 41 | LinkFlairTextColor string `json:"link_flair_text_color"` 42 | AuthorFlairBackgroundColor string `json:"author_flair_background_color"` 43 | SubredditType string `json:"subreddit_type"` 44 | Ups float64 `json:"ups"` 45 | TotalAwardsReceived float64 `json:"total_awards_received"` 46 | MediaEmbed []string `json:"media_embed"` 47 | ThumbnailWidth float64 `json:"thumbnail_width"` 48 | AuthorFlairTemplateId string `json:"author_flair_template_id"` 49 | IsOriginalContent bool `json:"is_original_content"` 50 | UserReports []interface{} `json:"user_reports"` 51 | SecureMedia string `json:"secure_media"` 52 | IsRedditMediaDomain bool `json:"is_reddit_media_domain"` 53 | IsMeta bool `json:"is_meta"` 54 | Category string `json:"category"` 55 | SecureMediaEmbed []string `json:"secure_media_embed"` 56 | LinkFlairText string `json:"link_flair_text"` 57 | CanModPost bool `json:"can_mod_post"` 58 | Score float64 `json:"score"` 59 | ApprovedBy string `json:"approved_by"` 60 | Thumbnail string `json:"thumbnail"` 61 | Edited bool `json:"edited"` 62 | AuthorFlairCssClass string `json:"author_flair_css_class"` 63 | AuthorFlairRichtext []string `json:"author_flair_richtext"` 64 | Gildings map[string]float64 `json:"gildings"` 65 | PostHint string `json:"post_hint"` 66 | ContentCategories []string `json:"content_categories"` 67 | IsSelf bool `json:"is_self"` 68 | ModNote string `json:"mod_note"` 69 | Created float64 `json:"created"` 70 | LinkFlairType string `json:"link_flair_type"` 71 | Wls float64 `json:"wls"` 72 | BannedBy string `json:"banned_by"` 73 | AuthorFlairType string `json:"author_flair_type"` 74 | Domain string `json:"domain"` 75 | SelftextHtml string `json:"selftext_html"` 76 | Likes float64 `json:"likes"` 77 | SuggestedSort string `json:"suggested_sort"` 78 | BannedAtUtc float64 `json:"banned_at_utc"` 79 | ViewCount float64 `json:"view_count"` 80 | Archived bool `json:"archived"` 81 | NoFollow bool `json:"no_follow"` 82 | IsCrosspostable bool `json:"is_crosspostable"` 83 | Pinned bool `json:"pinned"` 84 | Over18 bool `json:"over_18"` 85 | Preview PostPreview `json:"preview"` 86 | Awardings []PostAward `json:"all_awardings"` 87 | MediaOnly bool `json:"media_only"` 88 | CanGild bool `json:"can_gild"` 89 | Spoiler bool `json:"spoiler"` 90 | Locked bool `json:"locked"` 91 | AuthorFlairText string `json:"author_flair_text"` 92 | Visited bool `json:"visited"` 93 | NumReports float64 `json:"num_reports"` 94 | Distinguished bool `json:"distinguished"` 95 | SubredditId string `json:"subreddit_id"` 96 | ModReasonBy string `json:"mod_reason_by"` 97 | RemovalReason string `json:"removal_reason"` 98 | LinkFlairBackgroundColor string `json:"link_flair_background_color"` 99 | Id string `json:"id"` 100 | IsRobotIndexable bool `json:"is_robot_indexable"` 101 | ReportReasons string `json:"report_reasons"` // WILL BE REMOVED: as reddit's API always returns "This attribute is deprecated. Please use mod_reports and user_reports instead." now 102 | Author string `json:"author"` 103 | NumCrossposts float64 `json:"num_crossposts"` 104 | NumComments float64 `json:"num_comments"` 105 | SendReplies bool `json:"send_replies"` 106 | WhitelistStatus string `json:"whitelist_status"` 107 | ContestMode bool `json:"contest_mode"` 108 | ModReports []string `json:"mod_reports"` 109 | AuthorPatreonFlair bool `json:"author_patreon_flair"` 110 | AuthorFlairTextColor string `json:"author_flair_text_color"` 111 | Permalink string `json:"permalink"` 112 | ParentWhitelistStatus string `json:"parent_whitelist_status"` 113 | Stickied bool `json:"stickied"` 114 | Url string `json:"url"` 115 | SubredditSubscribers float64 `json:"subreddit_subscribers"` 116 | CreatedUtc float64 `json:"created_utc"` 117 | Media []string `json:"media"` 118 | IsVideo bool `json:"is_video"` 119 | } 120 | 121 | type PostAward struct { 122 | IsEnabled bool `json:"is_enabled"` 123 | Count float64 `json:"count"` 124 | SubredditId string `json:"subreddit_id"` 125 | Description string `json:"description"` 126 | CoinReward float64 `json:"coin_reward"` 127 | IconWidth float64 `json:"icon_width"` 128 | IconUrl string `json:"icon_url"` 129 | DaysOfPremium float64 `json:"days_of_premium"` 130 | IconHeight float64 `json:"icon_height"` 131 | ResizedIcons []PostAwardIcon `json:"resized_icons"` 132 | DaysOfDripExtension float64 `json:"days_of_drip_extension"` 133 | AwardType string `json:"award_type"` 134 | CoinPrice float64 `json:"coin_price"` 135 | Id string `json:"id"` 136 | Name string `json:"name"` 137 | } 138 | 139 | type PostAwardIcon struct { 140 | Url string `json:"url"` 141 | Width float64 `json:"width"` 142 | Height float64 `json:"height"` 143 | } 144 | 145 | type PostPreview struct { 146 | Images []PostPreviewImage `json:"images"` 147 | Enabled bool `json:"enabled"` 148 | } 149 | 150 | type PostPreviewImage struct { 151 | Source PostPreviewImageSource `json:"source"` 152 | Resolutions PostPreviewImageResolutions `json:"resolutions"` 153 | Variants []string `json:"variants"` 154 | Id string `json:"id"` 155 | } 156 | 157 | type PostPreviewImageResolutions struct { 158 | Url string `json:"url"` 159 | Width float64 `json:"width"` 160 | Height float64 `json:"height"` 161 | } 162 | 163 | type PostPreviewImageSource struct { 164 | Url string `json:"url"` 165 | Width float64 `json:"width"` 166 | Height float64 `json:"height"` 167 | } 168 | -------------------------------------------------------------------------------- /models/post_struct_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkCreatePostListing(b *testing.B) { 10 | data, _ := ioutil.ReadFile("./tests/postlisting.json") 11 | postListingExampleJson := string(data) 12 | for i := 0; i < b.N; i++ { 13 | sub := PostListing{} 14 | json.Unmarshal([]byte(postListingExampleJson), &sub) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/post_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func TestGetPostListingId(t *testing.T) { 10 | post := PostListing{} 11 | data, _ := ioutil.ReadFile("./tests/postlisting.json") 12 | json.Unmarshal(data, &post) 13 | if v := post.GetChildren()[0].GetId(); v != `t3_bev1v7` { 14 | t.Error( 15 | "For GetId()", 16 | "expected", `t3_bev1v7`, 17 | "got", v, 18 | ) 19 | } 20 | } 21 | 22 | func TestGetSubreddit(t *testing.T) { 23 | post := PostListing{} 24 | data, _ := ioutil.ReadFile("./tests/postlisting.json") 25 | json.Unmarshal(data, &post) 26 | if v := post.GetChildren()[0].GetSubreddit(); v != `MemeEconomy` { 27 | t.Error( 28 | "For GetSubreddit()", 29 | "expected", `MemeEconomy`, 30 | "got", v, 31 | ) 32 | } 33 | } 34 | 35 | func TestGetTitle(t *testing.T) { 36 | post := PostListing{} 37 | data, _ := ioutil.ReadFile("./tests/postlisting.json") 38 | json.Unmarshal(data, &post) 39 | if v := post.GetChildren()[1].GetTitle(); v != `Slow it down a bit, and invest here for THICC profits` { 40 | t.Error( 41 | "For GetTitle()", 42 | "expected", `Slow it down a bit, and invest here for THICC profits`, 43 | "got", v, 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /models/redditor.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | func (r Redditor) IsEmployee() bool { return r.Data.IsEmployee } 4 | func (r Redditor) GetName() string { return r.Data.Name } 5 | func (r Redditor) GetAuthor() string { return r.Data.Name } 6 | func (r Redditor) GetId() string { return r.Kind + "_" + r.Data.Id } 7 | func (r Redditor) GetParentId() string { return r.Kind + "_" + r.Data.Id } 8 | func (r Redditor) GetDescription() string { return r.Data.Subreddit.PublicDescription } 9 | func (r Redditor) GetCreated() float64 { return r.Data.Created } 10 | func (r Redditor) GetKarma() float64 { return r.Data.LinkKarma + r.Data.CommentKarma } 11 | func (r Redditor) GetUps() float64 { return r.Data.LinkKarma } 12 | func (r Redditor) GetDowns() float64 { return r.Data.CommentKarma } 13 | func (r Redditor) GetBody() string { return r.Data.Subreddit.PublicDescription } 14 | func (r Redditor) GetTitle() string { return r.Data.Subreddit.Title } 15 | func (r Redditor) GetFlair() string { return r.Data.Subreddit.PublicDescription } 16 | func (r Redditor) GetSubreddit() string { return r.Data.Subreddit.Name } 17 | func (r Redditor) GetUrl() string { return r.Data.Subreddit.IconImg } 18 | func (r Redditor) IsRoot() bool { return true } 19 | -------------------------------------------------------------------------------- /models/redditor_struct.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Redditor struct { 4 | Kind string `json:"kind"` 5 | Data RedditorData `json:"data"` 6 | } 7 | 8 | type RedditorData struct { 9 | IsEmployee bool `json:"is_employee"` 10 | IconImg string `json:"icon_img"` 11 | PrefShowSnoovatar bool `json:"pref_show_snoovatar"` 12 | Name string `json:"name"` 13 | IsFriend bool `json:"is_friend"` 14 | Created float64 `json:"created"` 15 | HasSubscribed bool `json:"has_subscribed"` 16 | HideFromRobots bool `json:"hide_from_robots"` 17 | CreatedUtc float64 `json:"created_utc"` 18 | LinkKarma float64 `json:"link_karma"` 19 | CommentKarma float64 `json:"comment_karma"` 20 | IsGold bool `json:"is_gold"` 21 | IsMod bool `json:"is_mod"` 22 | Verified bool `json:"verified"` 23 | Subreddit Subreddit_s `json:"subreddit"` 24 | HasVerifiedEmail bool `json:"has_verified_email"` 25 | Id string `json:"id"` 26 | } 27 | 28 | type Subreddit_s struct { 29 | DefaultSet bool `json:"default_set"` 30 | UserIsContributor bool `json:"user_is_contributor"` 31 | BannerImg string `json:"banner_img"` 32 | DisableContributorRequests bool `json:"disable_contributor_requests"` 33 | UserIsBanned bool `json:"user_is_banned"` 34 | FreeFormReports bool `json:"free_form_reports"` 35 | CommunityIcon string `json:"community_icon"` 36 | ShowMedia bool `json:"show_media"` 37 | IconColor string `json:"icon_color"` 38 | UserIsMuted bool `json:"user_is_muted"` 39 | DisplayName string `json:"display_name"` 40 | HeaderImg string `json:"header_img"` // * 41 | Title string `json:"title"` 42 | Over18 bool `json:"over_18"` 43 | IconSize []float64 `json:"icon_size"` 44 | PrimaryColor string `json:"primary_color"` 45 | IconImg string `json:"icon_img"` 46 | Description string `json:"description"` 47 | HeaderSize string `json:"header_size"` // * 48 | RestrictPosting bool `json:"restrict_posting"` 49 | RestrictCommenting bool `json:"restrict_commenting"` 50 | Subscribers float64 `json:"subscribers"` 51 | IsDefaultIcon bool `json:"is_default_icon"` 52 | LinkFlairPosition string `json:"link_flair_position"` 53 | DisplayNamePrefixed string `json:"display_name_prefixed"` 54 | KeyColor string `json:"key_color"` 55 | Name string `json:"name"` 56 | IsDefaultBanner bool `json:"is_default_banner"` 57 | Url string `json:"url"` 58 | BannerSize []float64 `json:"banner_size"` 59 | UserIsModerator bool `json:"user_is_moderator"` 60 | PublicDescription string `json:"public_description"` 61 | LinkFlairEnabled bool `json:"link_flair_enabled"` 62 | SubredditType string `json:"subreddit_type"` 63 | UserIsSubscriber bool `json:"user_is_subscriber"` 64 | } 65 | -------------------------------------------------------------------------------- /models/redditor_struct_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkCreateRedditor(b *testing.B) { 10 | data, _ := ioutil.ReadFile("./tests/redditor.json") 11 | redditorExampleJson := string(data) 12 | for i := 0; i < b.N; i++ { 13 | sub := Redditor{} 14 | json.Unmarshal([]byte(redditorExampleJson), &sub) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/reports.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | func (rl ReportListingChild) GetKind() string { return rl.Kind } 4 | func (rl *ReportListing) GetChildren() []ReportListingChild { return rl.Data.Children } 5 | func (rl ReportListingChild) GetId() string { 6 | return rl.Data.Name 7 | } 8 | func (rl *ReportListingChild) GetUserReports() []UserReport { 9 | reports := make([]UserReport, 0) 10 | for i := range rl.Data.UserReports { 11 | report := UserReport{} 12 | report.Reason = rl.Data.UserReports[i].([]interface{})[0].(string) 13 | report.NumOfReports = rl.Data.UserReports[i].([]interface{})[1].(float64) 14 | report.SnoozeStatus = rl.Data.UserReports[i].([]interface{})[2].(bool) 15 | report.CanSnooze = rl.Data.UserReports[i].([]interface{})[3].(bool) 16 | reports = append(reports, report) 17 | } 18 | return reports 19 | } 20 | -------------------------------------------------------------------------------- /models/reports_struct.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ReportListing struct { 4 | Kind string `json:"kind"` 5 | Data ReportListingData `json:"data"` 6 | } 7 | 8 | type ReportListingData struct { 9 | Modhash string `json:"modhash"` 10 | Dist float64 `json:"dist"` 11 | Children []ReportListingChild `json:"children"` 12 | } 13 | 14 | type ReportListingChild struct { 15 | Kind string `json:"kind"` 16 | Data PostListingChildData `json:"data"` 17 | } 18 | -------------------------------------------------------------------------------- /models/reports_struct_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkCreateReportListing(b *testing.B) { 10 | data, _ := ioutil.ReadFile("./tests/reports.json") 11 | reportListingExampleJson := string(data) 12 | for i := 0; i < b.N; i++ { 13 | sub := ReportListing{} 14 | json.Unmarshal([]byte(reportListingExampleJson), &sub) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/reports_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func TestGetReportListingId(t *testing.T) { 10 | report := ReportListing{} 11 | data, _ := ioutil.ReadFile("./tests/reports.json") 12 | json.Unmarshal(data, &report) 13 | if v := report.GetChildren()[0].GetId(); v != `t1_hw6ng9c` { 14 | t.Error( 15 | "For GetId()", 16 | "expected", `t1_hw6ng9c`, 17 | "got", v, 18 | ) 19 | } 20 | } 21 | 22 | func TestGetReportListingChildUserReports(t *testing.T) { 23 | report := ReportListing{} 24 | data, _ := ioutil.ReadFile("./tests/reports.json") 25 | json.Unmarshal(data, &report) 26 | first_post_reports := report.GetChildren()[0].GetUserReports() 27 | /* 28 | "user_reports": [ 29 | [ 30 | "Be Civil", 31 | 1, 32 | false, 33 | false 34 | ] 35 | ], 36 | */ 37 | if first_post_reports[0].NumOfReports != 1 { 38 | t.Error( 39 | "For first_post_reports[0].NumOfReports", 40 | "expected", 1, 41 | "got", first_post_reports[0].NumOfReports, 42 | ) 43 | } 44 | if first_post_reports[0].SnoozeStatus != false { 45 | t.Error( 46 | "For first_post_reports[0].SnoozeStatus", 47 | "expected", false, 48 | "got", first_post_reports[0].SnoozeStatus, 49 | ) 50 | } 51 | if first_post_reports[0].CanSnooze != false { 52 | t.Error( 53 | "For first_post_reports[0].CanSnooze", 54 | "expected", false, 55 | "got", first_post_reports[0].CanSnooze, 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /models/submission.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | func (s *Submission) GetId() string { return s.Json.Data.Name } 4 | func (s *Submission) GetDraftsCount() float64 { return s.Json.Data.DraftsCount } 5 | func (s *Submission) GetUrl() string { return s.Json.Data.Url } 6 | -------------------------------------------------------------------------------- /models/submission_struct.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Submission struct { 4 | Json SubmissionJson `json:"json"` 5 | } 6 | 7 | type SubmissionJson struct { 8 | Errors []string `json:"errors"` 9 | Data SubmissionJsonData `json:"data"` 10 | } 11 | 12 | type SubmissionJsonData struct { 13 | Url string `json:"url"` 14 | DraftsCount float64 `json:"drafts_count"` 15 | Id string `json:"id"` 16 | Name string `json:"name"` 17 | } 18 | -------------------------------------------------------------------------------- /models/submission_struct_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkCreateSubmission(b *testing.B) { 10 | data, _ := ioutil.ReadFile("./tests/submission.json") 11 | submissionExampleJson := string(data) 12 | for i := 0; i < b.N; i++ { 13 | sub := Submission{} 14 | json.Unmarshal([]byte(submissionExampleJson), &sub) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/subreddit.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | func (s Subreddit) GetId() string { return s.Data.Name } 4 | func (s Subreddit) GetParentId() string { return s.Data.Name } 5 | func (s Subreddit) GetName() string { return s.Data.Title } 6 | func (s Subreddit) GetAuthor() string { return s.Data.Title } 7 | func (s Subreddit) GetSubreddit() string { return s.Data.Name } 8 | func (s Subreddit) GetTitle() string { return s.Data.Title } 9 | func (s Subreddit) GetBody() string { return s.Data.Description } 10 | func (s Subreddit) GetDisplayName() string { return s.Data.DisplayName } 11 | func (s Subreddit) GetUrl() string { return s.Data.Url } 12 | func (s Subreddit) GetUps() float64 { return 0 } 13 | func (s Subreddit) GetKarma() float64 { return 0 } 14 | func (s Subreddit) GetDowns() float64 { return 0 } 15 | func (s Subreddit) IsOver18() bool { return s.Data.Over18 } 16 | func (s Subreddit) GetPublicDescription() string { return s.Data.PublicDescription } 17 | func (s Subreddit) GetDescription() string { return s.Data.Description } 18 | func (s Subreddit) GetFlair() string { return s.Data.HeaderTitle } 19 | func (s Subreddit) GetCreated() float64 { return s.Data.CreatedUtc } 20 | func (s Subreddit) GetSubscribers() float64 { return s.Data.Subscribers } 21 | func (s Subreddit) IsRoot() bool { return true } 22 | -------------------------------------------------------------------------------- /models/subreddit_struct.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Subreddit struct { 4 | Kind string `json:"kind"` 5 | Data SubredditData `json:"data"` 6 | } 7 | 8 | type SubredditData struct { 9 | UserFlairBackgroundColor string `json:"user_flair_background_color"` 10 | SubmitTextHtml string `json:"submit_text_html"` 11 | RestrictPosting bool `json:"restrict_posting"` 12 | UserIsBanned bool `json:"user_is_banned"` 13 | FreeFormReports bool `json:"free_form_reports"` 14 | WikiEnabled string `json:"wiki_enabled"` 15 | UserIsMuted bool `json:"user_is_muted"` 16 | UserCanFlairInSr string `json:"user_can_flair_in_sr"` 17 | DisplayName string `json:"display_name"` 18 | HeaderImg string `json:"header_img"` 19 | Title string `json:"title"` 20 | IconSize []float64 `json:"icon_size"` 21 | PrimaryColor string `json:"primary_color"` 22 | ActiveUserCount float64 `json:"active_user_count"` 23 | IconImg string `json:"icon_img"` 24 | AccountsActive float64 `json:"accounts_active"` 25 | PublicTraffic bool `json:"public_traffic"` 26 | Subscribers float64 `json:"subscribers"` 27 | UserFlairRichtext string `json:"user_flair_richtext"` 28 | VideostreamLinksCount float64 `json:"videostream_links_count"` 29 | Name string `json:"name"` 30 | Quarantine bool `json:"quarantine"` 31 | HideAds bool `json:"hide_ads"` 32 | EmojisEnabled bool `json:"emojis_enabled"` 33 | AdvertiserCategory string `json:"advertiser_category"` 34 | PublicDescription string `json:"public_description"` 35 | CommentScoreHideMins float64 `json:"comment_score_hide_mins"` 36 | UserHasFavorited bool `json:"user_has_favorited"` 37 | UserFlairTemplateId string `json:"user_flair_template_id"` 38 | CommunityIcon string `json:"community_icon"` 39 | BannerBackgroundImage string `json:"banner_background_image"` 40 | OriginalContentTagEnabled bool `json:"original_content_tag_enabled"` 41 | SubmitText string `json:"submit_text"` 42 | DescriptionHtml string `json:"description_html"` 43 | SpoilersEnabled bool `json:"spoilers_enabled"` 44 | HeaderTitle string `json:"header_title"` 45 | HeaderSize string `json:"header_size"` 46 | UserFlairPosition string `json:"user_flair_position"` 47 | AllOriginalContent bool `json:"all_original_content"` 48 | HasMenuWidget bool `json:"has_menu_widget"` 49 | IsEnrolledInNewModmail bool `json:"is_enrolled_in_new_modmail"` 50 | KeyColor string `json:"key_color"` 51 | EventPostsEnabled bool `json:"event_posts_enabled"` 52 | CanAssignUserFlair bool `json:"can_assign_user_flair"` 53 | Created float64 `json:"created"` 54 | Wls string `json:"wls"` 55 | ShowMediaPreview bool `json:"show_media_preview"` 56 | SubmissionType string `json:"submission_type"` 57 | UserIsSubscriber bool `json:"user_is_subscriber"` 58 | DisableContributorRequests bool `json:"disable_contributor_requests"` 59 | AllowVideogifs bool `json:"allow_videogifs"` 60 | UserFlairType string `json:"user_flair_type"` 61 | CollapseDeletedComments bool `json:"collapse_deleted_comments"` 62 | EmojisCustomSize string `json:"emojis_custom_size"` 63 | PublicDescriptionHtml string `json:"public_description_html"` 64 | AllowVideos bool `json:"allow_videos"` 65 | NotificationLevel string `json:"notification_level"` 66 | CanAssignLinkFlair bool `json:"can_assign_link_flair"` 67 | AccountsActiveIsFuzzed bool `json:"accounts_active_is_fuzzed"` 68 | SubmitTextLabel string `json:"submit_text_label"` 69 | LinkFlairPosition string `json:"link_flair_position"` 70 | UserSrFlairEnabled bool `json:"user_sr_flair_enabled"` 71 | UserFlairEnabledInSr bool `json:"user_flair_enabled_in_sr"` 72 | AllowDiscovery bool `json:"allow_discovery"` 73 | UserSrThemeEnabled bool `json:"user_sr_theme_enabled"` 74 | LinkFlairEnabled bool `json:"link_flair_enabled"` 75 | SubredditType string `json:"subreddit_type"` 76 | SuggestedCommentSort string `json:"suggested_comment_sort"` 77 | BannerImg string `json:"banner_img"` 78 | UserFlairText string `json:"user_flair_text"` 79 | BannerBackgroundColor string `json:"banner_background_color"` 80 | ShowMedia bool `json:"show_media"` 81 | Id string `json:"id"` 82 | UserIsModerator bool `json:"user_is_moderator"` 83 | Over18 bool `json:"over18"` 84 | Description string `json:"description"` 85 | SubmitLinkLabel string `json:"submit_link_label"` 86 | UserFlairTextColor string `json:"user_flair_text_color"` 87 | RestrictCommenting bool `json:"restrict_commenting"` 88 | UserFlairCssClass string `json:"user_flair_css_class"` 89 | AllowImages bool `json:"allow_images"` 90 | Lang string `json:"lang"` 91 | WhitelistStatus string `json:"whitelist_status"` 92 | Url string `json:"url"` 93 | CreatedUtc float64 `json:"created_utc"` 94 | BannerSize []float64 `json:"banner_size"` 95 | MobileBannerImage string `json:"mobile_banner_image"` 96 | UserIsContributor bool `json:"user_is_contributor"` 97 | } 98 | -------------------------------------------------------------------------------- /models/subreddit_struct_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkCreateSubreddit(b *testing.B) { 10 | data, _ := ioutil.ReadFile("./tests/subreddit.json") 11 | subredditExampleJson := string(data) 12 | for i := 0; i < b.N; i++ { 13 | sub := Subreddit{} 14 | json.Unmarshal([]byte(subredditExampleJson), &sub) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/subreddit_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func TestGetId(t *testing.T) { 10 | sub := Subreddit{} 11 | data, _ := ioutil.ReadFile("./tests/subreddit.json") 12 | json.Unmarshal(data, &sub) 13 | if v := sub.GetId(); v != `t5_m0je4` { 14 | t.Error( 15 | "For GetId()", 16 | "expected", `t5_m0je4`, 17 | "got", v, 18 | ) 19 | } 20 | } 21 | 22 | func TestGetName(t *testing.T) { 23 | sub := Subreddit{} 24 | data, _ := ioutil.ReadFile("./tests/subreddit.json") 25 | json.Unmarshal(data, &sub) 26 | if v := sub.GetName(); v != `MemeInvestor_bot` { 27 | t.Error( 28 | "For GetName()", 29 | "expected", `MemeInvestor_bot`, 30 | "got", v, 31 | ) 32 | } 33 | } 34 | 35 | func TestGetDisplayName(t *testing.T) { 36 | sub := Subreddit{} 37 | data, _ := ioutil.ReadFile("./tests/subreddit.json") 38 | json.Unmarshal(data, &sub) 39 | if v := sub.GetDisplayName(); v != `MemeInvestor_bot` { 40 | t.Error( 41 | "For GetDisplayName()", 42 | "expected", `MemeInvestor_bot`, 43 | "got", v, 44 | ) 45 | } 46 | } 47 | 48 | func TestGetUrl(t *testing.T) { 49 | sub := Subreddit{} 50 | data, _ := ioutil.ReadFile("./tests/subreddit.json") 51 | json.Unmarshal(data, &sub) 52 | if v := sub.GetUrl(); v != `/r/MemeInvestor_bot/` { 53 | t.Error( 54 | "For GetUrl()", 55 | "expected", "/r/MemeInvestor_bot/", 56 | "got", v, 57 | ) 58 | } 59 | } 60 | 61 | func TestIsOver18(t *testing.T) { 62 | sub := Subreddit{} 63 | data, _ := ioutil.ReadFile("./tests/subreddit.json") 64 | json.Unmarshal(data, &sub) 65 | if v := sub.IsOver18(); v != false { 66 | t.Error( 67 | "For IsOver18()", 68 | "expected", false, 69 | "got", v, 70 | ) 71 | } 72 | } 73 | 74 | func TestGetPublicDescription(t *testing.T) { 75 | sub := Subreddit{} 76 | data, _ := ioutil.ReadFile("./tests/subreddit.json") 77 | json.Unmarshal(data, &sub) 78 | if v := sub.GetPublicDescription(); v != "This subreddit is for questions, reports, or suggestions regarding /u/MemeInvestor_Bot. \n\nFor quick information see https://memes.market" { 79 | t.Error( 80 | "For GetPublicDescription()", 81 | "expected", "This subreddit is for questions, reports, or suggestions regarding /u/MemeInvestor_Bot. \n\nFor quick information see https://memes.market", 82 | "got", v, 83 | ) 84 | } 85 | } 86 | 87 | func TestGetDescription(t *testing.T) { 88 | sub := Subreddit{} 89 | data, _ := ioutil.ReadFile("./tests/subreddit.json") 90 | json.Unmarshal(data, &sub) 91 | if v := sub.GetDescription(); v != "######This is the official subreddit for the bot, /u/MemeInvestor_bot\n\n***\n\nHere you're encouraged to **report bugs, ask questions, and submit suggestions** regarding the bot. This subreddit is frequently viewed by the developers and, whether or not you receive a reply, it's very likely that your submission has been viewed and noted by someone on the team.\n\n***\n\n#####Rules:\n1. This is a no-meme subreddit. Only serious suggestions, reports, or questions allowed.\n\n2. All content must be regarding the bot. Keep it on-topic please.\n\n3. Be respectful. We're all nice people here.\n\n***\n\n&nbsp;\n\n####^(Please don't send a message before first submitting your post on the subreddit.)\n\n######**[Message us anyway.](https://www.reddit.com/message/compose?to=%2Fr%2FMemeInvestor_Bot)**" { 92 | t.Error( 93 | "For GetDescription()", 94 | "expected", "######This is the official subreddit for the bot, /u/MemeInvestor_bot\n\n***\n\nHere you're encouraged to **report bugs, ask questions, and submit suggestions** regarding the bot. This subreddit is frequently viewed by the developers and, whether or not you receive a reply, it's very likely that your submission has been viewed and noted by someone on the team.\n\n***\n\n#####Rules:\n1. This is a no-meme subreddit. Only serious suggestions, reports, or questions allowed.\n\n2. All content must be regarding the bot. Keep it on-topic please.\n\n3. Be respectful. We're all nice people here.\n\n***\n\n&nbsp;\n\n####^(Please don't send a message before first submitting your post on the subreddit.)\n\n######**[Message us anyway.](https://www.reddit.com/message/compose?to=%2Fr%2FMemeInvestor_Bot)**", 95 | "got", v, 96 | ) 97 | } 98 | } 99 | 100 | func TestGetCreated(t *testing.T) { 101 | sub := Subreddit{} 102 | data, _ := ioutil.ReadFile("./tests/subreddit.json") 103 | json.Unmarshal(data, &sub) 104 | if v := sub.GetCreated(); v != 1532014741.0 { 105 | t.Error( 106 | "For GetCreated()", 107 | "expected", 1532014741.0, 108 | "got", v, 109 | ) 110 | } 111 | } 112 | 113 | func TestGetSubscribers(t *testing.T) { 114 | sub := Subreddit{} 115 | data, _ := ioutil.ReadFile("./tests/subreddit.json") 116 | json.Unmarshal(data, &sub) 117 | if v := sub.GetSubscribers(); v != 1339 { 118 | t.Error( 119 | "For GetSubscribers()", 120 | "expected", 1339, 121 | "got", v, 122 | ) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /models/tests/comment.json: -------------------------------------------------------------------------------- 1 | {"json": {"errors": [], "data": {"things": [{"kind": "t1", "data": {"author_flair_background_color": null, "total_awards_received": 0, "approved_at_utc": null, "distinguished": null, "mod_reason_by": null, "banned_by": null, "author_flair_type": "text", "removal_reason": null, "link_id": "t3_bopvxw", "author_flair_template_id": null, "likes": true, "replies": "", "user_reports": [], "saved": false, "id": "enj4q14", "banned_at_utc": null, "mod_reason_title": null, "gilded": 0, "archived": false, "no_follow": false, "author": "thecswbot", "rte_mode": "markdown", "can_mod_post": true, "created_utc": 1557874832.0, "send_replies": true, "parent_id": "t3_bopvxw", "score": 1, "author_fullname": "t2_knsbywx", "approved_by": null, "mod_note": null, "all_awardings": [], "subreddit_id": "t5_jke8s", "body": "My First Comment", "edited": false, "gildings": {}, "author_flair_css_class": null, "name": "t1_enj4q14", "author_patreon_flair": false, "downs": 0, "author_flair_richtext": [], "is_submitter": true, "collapsed_reason": null, "body_html": "<div class=\"md\"><p>My First Comment</p>\n</div>", "stickied": false, "can_gild": false, "removed": false, "approved": false, "author_flair_text_color": null, "score_hidden": false, "permalink": "/r/memeinvestor_test/comments/bopvxw/mypost/enj4q14/", "num_reports": 0, "locked": false, "report_reasons": [], "created": 1557903632.0, "subreddit": "memeinvestor_test", "author_flair_text": null, "spam": false, "collapsed": false, "subreddit_name_prefixed": "r/memeinvestor_test", "controversiality": 0, "ignore_reports": false, "mod_reports": [], "subreddit_type": "public", "ups": 1}}]}}} 2 | -------------------------------------------------------------------------------- /models/tests/commentlisting.json: -------------------------------------------------------------------------------- 1 | {"kind": "Listing", "data": {"modhash": null, "dist": 2, "children": [{"kind": "t1", "data": {"first_message": null, "first_message_name": null, "subreddit": "memeinvestor_test", "likes": null, "replies": "", "id": "entzb9q", "subject": "comment reply", "was_comment": true, "score": 1, "author": "Thecsw", "num_comments": 37, "parent_id": "t1_entywk0", "subreddit_name_prefixed": "r/memeinvestor_test", "new": true, "body": "!balance", "link_title": "mypost", "dest": "thecswbot", "body_html": "<!-- SC_OFF --><div class=\"md\"><p>!balance</p>\n</div><!-- SC_ON -->", "name": "t1_entzb9q", "created": 1558078659.0, "created_utc": 1558049859.0, "context": "/r/memeinvestor_test/comments/bo553g/mypost/entzb9q/?context=3", "distinguished": null}}, {"kind": "t1", "data": {"first_message": null, "first_message_name": null, "subreddit": "memeinvestor_test", "likes": null, "replies": "", "id": "enmvyru", "subject": "post reply", "was_comment": true, "score": 1, "author": "rakorako404", "num_comments": 2, "parent_id": "t3_bopvuw", "subreddit_name_prefixed": "r/memeinvestor_test", "new": true, "body": "my comment!", "link_title": "mypost", "dest": "thecswbot", "body_html": "<!-- SC_OFF --><div class=\"md\"><p>my comment!</p>\n</div><!-- SC_ON -->", "name": "t1_enmvyru", "created": 1557966435.0, "created_utc": 1557937635.0, "context": "/r/memeinvestor_test/comments/bopvuw/mypost/enmvyru/?context=3", "distinguished": null}}], "after": null, "before": null}} 2 | -------------------------------------------------------------------------------- /models/tests/me.json: -------------------------------------------------------------------------------- 1 | {"is_employee": false, "seen_layout_switch": false, "has_visited_new_profile": true, "pref_no_profanity": true, "has_external_account": false, "pref_geopopular": "", "seen_redesign_modal": false, "pref_show_trending": true, "subreddit": {"default_set": true, "user_is_contributor": false, "banner_img": "", "disable_contributor_requests": false, "user_is_banned": false, "free_form_reports": true, "community_icon": "", "show_media": true, "icon_color": "#7E53C1", "user_is_muted": false, "display_name": "u_HiveWriting_bot", "header_img": null, "title": "", "over_18": false, "icon_size": [256, 256], "primary_color": "", "icon_img": "https://www.redditstatic.com/avatars/avatar_default_06_7E53C1.png", "description": "", "header_size": null, "restrict_posting": true, "restrict_commenting": false, "subscribers": 1, "is_default_icon": true, "link_flair_position": "", "display_name_prefixed": "u/HiveWriting_bot", "key_color": "", "name": "t5_zhwst", "is_default_banner": true, "url": "/user/HiveWriting_bot/", "banner_size": null, "user_is_moderator": true, "public_description": "", "link_flair_enabled": false, "subreddit_type": "user", "user_is_subscriber": false}, "is_sponsor": false, "gold_expiration": null, "has_gold_subscription": false, "num_friends": 0, "features": {"richtext_previews": true, "do_not_track": true, "chat_subreddit": true, "chat": true, "seq_randomize_sort": true, "sequence": true, "show_amp_link": true, "mweb_xpromo_revamp_v2": {"owner": "growth", "variant": "treatment_6", "experiment_id": 457}, "mweb_xpromo_interstitial_comments_ios": true, "chat_reddar_reports": true, "mweb_xpromo_modal_listing_click_daily_dismissible_ios": true, "chat_rollout": true, "mweb_xpromo_interstitial_comments_android": true, "chat_group_rollout": true, "mweb_link_tab": {"owner": "growth", "variant": "control_1", "experiment_id": 404}, "spez_modal": true, "mweb_xpromo_modal_listing_click_daily_dismissible_android": true, "community_awards": true, "default_srs_holdout": {"owner": "relevance", "variant": "popular", "experiment_id": 171}, "chat_user_settings": true, "dual_write_user_prefs": true}, "has_android_subscription": false, "verified": true, "new_modmail_exists": false, "pref_autoplay": true, "coins": 0, "has_paypal_subscription": false, "has_subscribed_to_premium": false, "id": "3kmf5q59", "has_stripe_subscription": false, "seen_premium_adblock_modal": false, "can_create_subreddit": true, "over_18": false, "is_gold": false, "is_mod": true, "suspension_expiration_utc": null, "has_verified_email": true, "is_suspended": false, "pref_video_autoplay": true, "in_chat": true, "in_redesign_beta": false, "icon_img": "https://www.redditstatic.com/avatars/avatar_default_06_7E53C1.png", "has_mod_mail": false, "pref_nightmode": false, "oauth_client_id": "PMQYCRTBZq6qHw", "hide_from_robots": false, "link_karma": 192, "force_password_reset": false, "inbox_count": 124, "pref_top_karma_subreddits": true, "has_mail": true, "pref_show_snoovatar": false, "name": "HiveWriting_bot", "pref_clickgadget": 5, "created": 1554873692.0, "gold_creddits": 0, "created_utc": 1554844892.0, "has_ios_subscription": false, "pref_show_twitter": false, "in_beta": false, "comment_karma": 7, "has_subscribed": true, "seen_subreddit_chat_ftux": false} 2 | -------------------------------------------------------------------------------- /models/tests/redditor.json: -------------------------------------------------------------------------------- 1 | {"kind": "t2", "data": {"is_employee": false, "icon_img": "https://a.thumbs.redditmedia.com/7kijh7qQbAFMWNG0y5k1Jg8FARuO_tsYshB13cq9Ah4.png", "pref_show_snoovatar": false, "name": "Thecsw", "is_friend": false, "created": 1427243659.0, "has_subscribed": true, "hide_from_robots": false, "created_utc": 1427214859.0, "link_karma": 4338, "comment_karma": 1972, "is_gold": false, "is_mod": true, "verified": true, "subreddit": {"default_set": true, "user_is_contributor": false, "banner_img": "https://b.thumbs.redditmedia.com/HmQ2FMp9qK8GK_-j-wRTQKaScG6hOcNz0cSYa2fopxY.png", "disable_contributor_requests": false, "user_is_banned": false, "free_form_reports": true, "community_icon": "", "show_media": true, "icon_color": "", "user_is_muted": false, "display_name": "u_Thecsw", "header_img": null, "title": "thecsw", "over_18": false, "icon_size": [256, 256], "primary_color": "", "icon_img": "https://a.thumbs.redditmedia.com/7kijh7qQbAFMWNG0y5k1Jg8FARuO_tsYshB13cq9Ah4.png", "description": "", "header_size": null, "restrict_posting": true, "restrict_commenting": false, "subscribers": 0, "is_default_icon": false, "link_flair_position": "", "display_name_prefixed": "u/Thecsw", "key_color": "", "name": "t5_3p2jb", "is_default_banner": false, "url": "/user/Thecsw/", "banner_size": [1280, 384], "user_is_moderator": false, "public_description": "GIT d++ s+:- a--- C++ UL P! L++ E W+++ N+++ o K- w++ O--- M- V PS PE+ Y+ PGP++ t 5- X++ R !tv b+++ DI+ D+ \nG++ e++ h r y", "link_flair_enabled": false, "subreddit_type": "user", "user_is_subscriber": false}, "has_verified_email": true, "id": "mglj6"}} 2 | -------------------------------------------------------------------------------- /models/tests/reports.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "Listing", 3 | "data": { 4 | "after": null, 5 | "dist": 3, 6 | "modhash": "t6qursuix3ebe255effc77cc42a1224053a92508f728892446", 7 | "geo_filter": "", 8 | "children": [ 9 | { 10 | "kind": "t1", 11 | "data": { 12 | "awarders": [], 13 | "subreddit_id": "t5_2ql2m", 14 | "approved_at_utc": null, 15 | "author_is_blocked": false, 16 | "comment_type": null, 17 | "edited": false, 18 | "mod_reason_by": null, 19 | "banned_by": null, 20 | "author_flair_type": "text", 21 | "total_awards_received": 0, 22 | "subreddit": "Egypt", 23 | "removed": false, 24 | "link_author": "raddit2233", 25 | "likes": null, 26 | "replies": "", 27 | "user_reports": [ 28 | [ 29 | "Be Civil", 30 | 1, 31 | false, 32 | false 33 | ] 34 | ], 35 | "saved": false, 36 | "id": "hw6ng9c", 37 | "banned_at_utc": null, 38 | "mod_reason_title": null, 39 | "gilded": 0, 40 | "archived": false, 41 | "collapsed_reason_code": null, 42 | "no_follow": true, 43 | "spam": false, 44 | "num_comments": 46, 45 | "treatment_tags": [], 46 | "can_mod_post": true, 47 | "ignore_reports": false, 48 | "send_replies": true, 49 | "parent_id": "t3_siezes", 50 | "score": 1, 51 | "author_fullname": "t2_7ylm97ef", 52 | "over_18": false, 53 | "removal_reason": null, 54 | "approved_by": null, 55 | "mod_note": null, 56 | "controversiality": 0, 57 | "collapsed": false, 58 | "body": "\u0645\u064a\u0646 \u062f\u0627 \u0627\u0644\u0644\u064a \u063a\u0636\u0628\u0627\u0646 \u064a\u0627 \u0643\u0633 \u0627\u0645\u0643\u061f", 59 | "link_title": "Why are we so angry?", 60 | "top_awarded_type": null, 61 | "author_flair_css_class": null, 62 | "name": "t1_hw6ng9c", 63 | "author_patreon_flair": false, 64 | "downs": 0, 65 | "author_flair_richtext": [], 66 | "is_submitter": false, 67 | "body_html": "<div class=\"md\"><p>\u0645\u064a\u0646 \u062f\u0627 \u0627\u0644\u0644\u064a \u063a\u0636\u0628\u0627\u0646 \u064a\u0627 \u0643\u0633 \u0627\u0645\u0643\u061f</p>\n</div>", 68 | "gildings": {}, 69 | "collapsed_reason": null, 70 | "associated_award": null, 71 | "stickied": false, 72 | "author_premium": false, 73 | "can_gild": true, 74 | "link_id": "t3_siezes", 75 | "unrepliable_reason": null, 76 | "approved": false, 77 | "author_flair_text_color": null, 78 | "all_awardings": [], 79 | "score_hidden": false, 80 | "permalink": "/r/Egypt/comments/siezes/why_are_we_so_angry/hw6ng9c/", 81 | "subreddit_type": "public", 82 | "link_permalink": "https://www.reddit.com/r/Egypt/comments/siezes/why_are_we_so_angry/", 83 | "report_reasons": [ 84 | "This attribute is deprecated. Please use mod_reports and user_reports instead." 85 | ], 86 | "created": 1644381422.0, 87 | "author_flair_text": null, 88 | "link_url": "https://i.redd.it/ur36ceelgcf81.png", 89 | "author": "Aggravating-Ad860", 90 | "created_utc": 1644381422.0, 91 | "subreddit_name_prefixed": "r/Egypt", 92 | "ups": 1, 93 | "locked": false, 94 | "author_flair_background_color": null, 95 | "collapsed_because_crowd_control": null, 96 | "mod_reports": [], 97 | "quarantine": false, 98 | "num_reports": 1, 99 | "distinguished": null, 100 | "author_flair_template_id": null 101 | } 102 | }, 103 | { 104 | "kind": "t1", 105 | "data": { 106 | "awarders": [], 107 | "subreddit_id": "t5_2ql2m", 108 | "approved_at_utc": null, 109 | "author_is_blocked": false, 110 | "comment_type": null, 111 | "edited": false, 112 | "mod_reason_by": null, 113 | "banned_by": null, 114 | "author_flair_type": "text", 115 | "total_awards_received": 0, 116 | "subreddit": "Egypt", 117 | "removed": false, 118 | "link_author": "killyanred10", 119 | "likes": null, 120 | "replies": "", 121 | "user_reports": [ 122 | [ 123 | "Be Civil", 124 | 1, 125 | false, 126 | false 127 | ] 128 | ], 129 | "saved": false, 130 | "id": "hw5tpls", 131 | "banned_at_utc": null, 132 | "mod_reason_title": null, 133 | "gilded": 0, 134 | "archived": false, 135 | "collapsed_reason_code": null, 136 | "no_follow": true, 137 | "spam": false, 138 | "num_comments": 72, 139 | "treatment_tags": [], 140 | "can_mod_post": true, 141 | "ignore_reports": false, 142 | "send_replies": true, 143 | "parent_id": "t1_hw5ssit", 144 | "score": 1, 145 | "author_fullname": "t2_agnvth5s", 146 | "over_18": false, 147 | "removal_reason": null, 148 | "approved_by": null, 149 | "mod_note": null, 150 | "controversiality": 0, 151 | "collapsed": false, 152 | "body": "\u064a\u0627\u0639\u0645 \u0641\u064a\u0647 \u0646\u0627\u0633 \u0645\u0635\u0631\u064a\u064a\u0646 \u0644\u0644\u0627\u0633\u0641 \u0645\u0639\u062f\u0648\u0645\u064a\u0646 \u0627\u0644\u0643\u0631\u0627\u0645\u0647 \u0648\u0644\u0648 \u062d\u062f \u063a\u0631\u064a\u0628 \u0628\u0635\u0642 \u0641\u0649 \u0648\u062c\u0648\u0647\u0647\u0645 \u0628\u0631\u062f\u0648 \u062d\u064a\u0642\u0648\u0644\u0648 \u0627\u062d\u0646\u0627 \u0627\u0644\u0649 \u0648\u062d\u0634\u064a\u0646 .......... Cuckold.....\u0628\u0639\u064a\u062f \u0639\u0646\u0643", 153 | "link_title": "\u0645\u0627 \u0645\u062f\u064a \u062a\u0623\u064a\u064a\u062f\u0643 \u0644\u0648\u062d\u062f\u0629 (\u0641\u064a\u062f\u0631\u0627\u0644\u064a\u0629,\u0643\u0648\u0646\u0641\u064a\u062f\u0631\u0627\u0644\u064a\u0629\u060c \u0643\u0627\u0645\u0644\u0629)\u0628\u064a\u0646 \u062c\u0645\u0647\u0648\u0631\u064a\u0629 \u0627\u0644\u0633\u0648\u062f\u0627\u0646 \u0648 \u0648\u062c\u0645\u0647\u0648\u0631\u064a\u0629 \u0645\u0635\u0631 \u0627\u0644\u0639\u0631\u0628\u064a\u0629", 154 | "top_awarded_type": null, 155 | "author_flair_css_class": null, 156 | "name": "t1_hw5tpls", 157 | "author_patreon_flair": false, 158 | "downs": 0, 159 | "author_flair_richtext": [], 160 | "is_submitter": false, 161 | "body_html": "<div class=\"md\"><p>\u064a\u0627\u0639\u0645 \u0641\u064a\u0647 \u0646\u0627\u0633 \u0645\u0635\u0631\u064a\u064a\u0646 \u0644\u0644\u0627\u0633\u0641 \u0645\u0639\u062f\u0648\u0645\u064a\u0646 \u0627\u0644\u0643\u0631\u0627\u0645\u0647 \u0648\u0644\u0648 \u062d\u062f \u063a\u0631\u064a\u0628 \u0628\u0635\u0642 \u0641\u0649 \u0648\u062c\u0648\u0647\u0647\u0645 \u0628\u0631\u062f\u0648 \u062d\u064a\u0642\u0648\u0644\u0648 \u0627\u062d\u0646\u0627 \u0627\u0644\u0649 \u0648\u062d\u0634\u064a\u0646 .......... Cuckold.....\u0628\u0639\u064a\u062f \u0639\u0646\u0643</p>\n</div>", 162 | "gildings": {}, 163 | "collapsed_reason": null, 164 | "associated_award": null, 165 | "stickied": false, 166 | "author_premium": false, 167 | "can_gild": true, 168 | "link_id": "t3_snxx7o", 169 | "unrepliable_reason": null, 170 | "approved": false, 171 | "author_flair_text_color": null, 172 | "all_awardings": [], 173 | "score_hidden": false, 174 | "permalink": "/r/Egypt/comments/snxx7o/\u0645\u0627_\u0645\u062f\u064a_\u062a\u0623\u064a\u064a\u062f\u0643_\u0644\u0648\u062d\u062f\u0629_\u0641\u064a\u062f\u0631\u0627\u0644\u064a\u0629\u0643\u0648\u0646\u0641\u064a\u062f\u0631\u0627\u0644\u064a\u0629_\u0643\u0627\u0645\u0644\u0629\u0628\u064a\u0646/hw5tpls/", 175 | "subreddit_type": "public", 176 | "link_permalink": "https://www.reddit.com/r/Egypt/comments/snxx7o/\u0645\u0627_\u0645\u062f\u064a_\u062a\u0623\u064a\u064a\u062f\u0643_\u0644\u0648\u062d\u062f\u0629_\u0641\u064a\u062f\u0631\u0627\u0644\u064a\u0629\u0643\u0648\u0646\u0641\u064a\u062f\u0631\u0627\u0644\u064a\u0629_\u0643\u0627\u0645\u0644\u0629\u0628\u064a\u0646/", 177 | "report_reasons": [ 178 | "This attribute is deprecated. Please use mod_reports and user_reports instead." 179 | ], 180 | "created": 1644368437.0, 181 | "author_flair_text": null, 182 | "link_url": "https://www.reddit.com/r/Egypt/comments/snxx7o/\u0645\u0627_\u0645\u062f\u064a_\u062a\u0623\u064a\u064a\u062f\u0643_\u0644\u0648\u062d\u062f\u0629_\u0641\u064a\u062f\u0631\u0627\u0644\u064a\u0629\u0643\u0648\u0646\u0641\u064a\u062f\u0631\u0627\u0644\u064a\u0629_\u0643\u0627\u0645\u0644\u0629\u0628\u064a\u0646/", 183 | "author": "ArtisticAd9562", 184 | "created_utc": 1644368437.0, 185 | "subreddit_name_prefixed": "r/Egypt", 186 | "ups": 1, 187 | "locked": false, 188 | "author_flair_background_color": null, 189 | "collapsed_because_crowd_control": null, 190 | "mod_reports": [], 191 | "quarantine": false, 192 | "num_reports": 1, 193 | "distinguished": null, 194 | "author_flair_template_id": null 195 | } 196 | }, 197 | { 198 | "kind": "t1", 199 | "data": { 200 | "awarders": [], 201 | "subreddit_id": "t5_2ql2m", 202 | "approved_at_utc": null, 203 | "author_is_blocked": false, 204 | "comment_type": null, 205 | "edited": false, 206 | "mod_reason_by": null, 207 | "banned_by": null, 208 | "author_flair_type": "text", 209 | "total_awards_received": 0, 210 | "subreddit": "Egypt", 211 | "removed": false, 212 | "link_author": "aniazah2003", 213 | "likes": null, 214 | "replies": "", 215 | "user_reports": [ 216 | [ 217 | "It's promoting hate based on identity or vulnerability", 218 | 1, 219 | false, 220 | false 221 | ] 222 | ], 223 | "saved": false, 224 | "id": "hw3xy9h", 225 | "banned_at_utc": null, 226 | "mod_reason_title": null, 227 | "gilded": 0, 228 | "archived": false, 229 | "collapsed_reason_code": null, 230 | "no_follow": true, 231 | "spam": false, 232 | "num_comments": 151, 233 | "treatment_tags": [], 234 | "can_mod_post": true, 235 | "ignore_reports": false, 236 | "send_replies": true, 237 | "parent_id": "t3_snkoqm", 238 | "score": 0, 239 | "author_fullname": "t2_wudlk", 240 | "over_18": false, 241 | "removal_reason": null, 242 | "approved_by": null, 243 | "mod_note": null, 244 | "controversiality": 0, 245 | "collapsed": false, 246 | "body": "I dont understand why the holocaust is considered such a big taboo when the us,uk and the delusional jews themselves did crimes that are the same if not worse", 247 | "link_title": "Is it me or is this racist?", 248 | "top_awarded_type": null, 249 | "author_flair_css_class": null, 250 | "name": "t1_hw3xy9h", 251 | "author_patreon_flair": false, 252 | "downs": 0, 253 | "author_flair_richtext": [], 254 | "is_submitter": false, 255 | "body_html": "<div class=\"md\"><p>I dont understand why the holocaust is considered such a big taboo when the us,uk and the delusional jews themselves did crimes that are the same if not worse</p>\n</div>", 256 | "gildings": {}, 257 | "collapsed_reason": null, 258 | "associated_award": null, 259 | "stickied": false, 260 | "author_premium": false, 261 | "can_gild": true, 262 | "link_id": "t3_snkoqm", 263 | "unrepliable_reason": null, 264 | "approved": false, 265 | "author_flair_text_color": null, 266 | "all_awardings": [], 267 | "score_hidden": false, 268 | "permalink": "/r/Egypt/comments/snkoqm/is_it_me_or_is_this_racist/hw3xy9h/", 269 | "subreddit_type": "public", 270 | "link_permalink": "https://www.reddit.com/r/Egypt/comments/snkoqm/is_it_me_or_is_this_racist/", 271 | "report_reasons": [ 272 | "This attribute is deprecated. Please use mod_reports and user_reports instead." 273 | ], 274 | "created": 1644342572.0, 275 | "author_flair_text": null, 276 | "link_url": "https://www.reddit.com/r/Egypt/comments/snkoqm/is_it_me_or_is_this_racist/", 277 | "author": "xAshwal", 278 | "created_utc": 1644342572.0, 279 | "subreddit_name_prefixed": "r/Egypt", 280 | "ups": 0, 281 | "locked": false, 282 | "author_flair_background_color": null, 283 | "collapsed_because_crowd_control": null, 284 | "mod_reports": [], 285 | "quarantine": false, 286 | "num_reports": 1, 287 | "distinguished": null, 288 | "author_flair_template_id": null 289 | } 290 | } 291 | ], 292 | "before": null 293 | } 294 | } -------------------------------------------------------------------------------- /models/tests/submission.json: -------------------------------------------------------------------------------- 1 | {"json": {"errors": [], "data": {"url": "https://www.reddit.com/r/memeinvestor_test/comments/bod8sf/mypost/", "drafts_count": 0, "id": "bod8sf", "name": "t3_bod8sf"}}} 2 | -------------------------------------------------------------------------------- /models/tests/subreddit.json: -------------------------------------------------------------------------------- 1 | {"kind": "t5","data": {"user_flair_background_color": null,"submit_text_html": null,"restrict_posting": true,"user_is_banned": false,"free_form_reports": true,"wiki_enabled": null,"user_is_muted": false,"user_can_flair_in_sr": null,"display_name": "MemeInvestor_bot","header_img": null,"title": "MemeInvestor_bot","icon_size": [256,256],"primary_color": "","active_user_count": 2,"icon_img": "https:\/\/b.thumbs.redditmedia.com\/bz7E9y0Ca7UZS7QBCesV2aRavkZ9ZydYBYdsqRzdY1A.png","display_name_prefixed": "r\/MemeInvestor_bot","accounts_active": 2,"public_traffic": false,"subscribers": 1339,"user_flair_richtext": [],"videostream_links_count": 0,"name": "t5_m0je4","quarantine": false,"hide_ads": false,"emojis_enabled": false,"advertiser_category": "","public_description": "This subreddit is for questions, reports, or suggestions regarding \/u\/MemeInvestor_Bot. \n\nFor quick information see https:\/\/memes.market","comment_score_hide_mins": 0,"user_has_favorited": false,"user_flair_template_id": null,"community_icon": "","banner_background_image": "https:\/\/styles.redditmedia.com\/t5_m0je4\/styles\/bannerBackgroundImage_nin8va4paya11.png","original_content_tag_enabled": false,"submit_text": "","description_html": "<!-- SC_OFF --><div class=\"md\"><h6>This is the official subreddit for the bot, <a href=\"\/u\/MemeInvestor_bot\">\/u\/MemeInvestor_bot<\/a><\/h6>\n\n<hr\/>\n\n<p>Here you&#39;re encouraged to <strong>report bugs, ask questions, and submit suggestions<\/strong> regarding the bot. This subreddit is frequently viewed by the developers and, whether or not you receive a reply, it&#39;s very likely that your submission has been viewed and noted by someone on the team.<\/p>\n\n<hr\/>\n\n<h5>Rules:<\/h5>\n\n<ol>\n<li><p>This is a no-meme subreddit. Only serious suggestions, reports, or questions allowed.<\/p><\/li>\n<li><p>All content must be regarding the bot. Keep it on-topic please.<\/p><\/li>\n<li><p>Be respectful. We&#39;re all nice people here.<\/p><\/li>\n<\/ol>\n\n<hr\/>\n\n<p>&nbsp;<\/p>\n\n<h4><sup>Please don&#39;t send a message before first submitting your post on the subreddit.<\/sup><\/h4>\n\n<h6><strong><a href=\"https:\/\/www.reddit.com\/message\/compose?to=%2Fr%2FMemeInvestor_Bot\">Message us anyway.<\/a><\/strong><\/h6>\n<\/div><!-- SC_ON -->","spoilers_enabled": true,"header_title": null,"header_size": null,"user_flair_position": "right","all_original_content": false,"has_menu_widget": false,"is_enrolled_in_new_modmail": null,"key_color": "#545452","can_assign_user_flair": false,"created": 1532043541.0,"wls": null,"show_media_preview": true,"submission_type": "any","user_is_subscriber": false,"disable_contributor_requests": false,"allow_videogifs": true,"user_flair_type": "text","collapse_deleted_comments": false,"emojis_custom_size": null,"public_description_html": "<!-- SC_OFF --><div class=\"md\"><p>This subreddit is for questions, reports, or suggestions regarding <a href=\"\/u\/MemeInvestor_Bot\">\/u\/MemeInvestor_Bot<\/a>. <\/p>\n\n<p>For quick information see <a href=\"https:\/\/memes.market\">https:\/\/memes.market<\/a><\/p>\n<\/div><!-- SC_ON -->","allow_videos": true,"notification_level": null,"can_assign_link_flair": true,"accounts_active_is_fuzzed": false,"submit_text_label": null,"link_flair_position": "right","user_sr_flair_enabled": true,"user_flair_enabled_in_sr": true,"allow_discovery": true,"user_sr_theme_enabled": true,"link_flair_enabled": true,"subreddit_type": "public","suggested_comment_sort": null,"banner_img": "https:\/\/b.thumbs.redditmedia.com\/Wqk7-zU-_lHm1-Oqd-Pg6fnShz2tKwNiFkV9Y23mwCM.png","user_flair_text": null,"banner_background_color": "","show_media": true,"id": "m0je4","user_is_moderator": false,"over18": false,"description": "######This is the official subreddit for the bot, \/u\/MemeInvestor_bot\n\n***\n\nHere you're encouraged to **report bugs, ask questions, and submit suggestions** regarding the bot. This subreddit is frequently viewed by the developers and, whether or not you receive a reply, it's very likely that your submission has been viewed and noted by someone on the team.\n\n***\n\n#####Rules:\n1. This is a no-meme subreddit. Only serious suggestions, reports, or questions allowed.\n\n2. All content must be regarding the bot. Keep it on-topic please.\n\n3. Be respectful. We're all nice people here.\n\n***\n\n&nbsp;\n\n####^(Please don't send a message before first submitting your post on the subreddit.)\n\n######**[Message us anyway.](https:\/\/www.reddit.com\/message\/compose?to=%2Fr%2FMemeInvestor_Bot)**","submit_link_label": null,"user_flair_text_color": null,"restrict_commenting": false,"user_flair_css_class": null,"allow_images": true,"lang": "en","whitelist_status": null,"url": "\/r\/MemeInvestor_bot\/","created_utc": 1532014741.0,"banner_size": [654,196],"mobile_banner_image": "","user_is_contributor": false}} 2 | -------------------------------------------------------------------------------- /models/user_reports.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type UserReport struct { 4 | Reason string 5 | NumOfReports float64 6 | SnoozeStatus bool 7 | CanSnooze bool 8 | } 9 | -------------------------------------------------------------------------------- /modqueue.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/thecsw/mira/v4/models" 11 | ) 12 | 13 | // ModQueue returns modqueue entries from a subreddit up to a specified limit sorted by the given parameters 14 | // Limit is any numerical value, so 0 <= limit <= 100. 15 | func (c *Reddit) ModQueue(limit int) ([]models.ModQueueListingChild, error) { 16 | name, ttype := c.getQueue() 17 | switch ttype { 18 | case subredditType: 19 | return c.getSubredditModQueue(name, limit) 20 | default: 21 | return nil, fmt.Errorf("'%s' type does not have an option for modqueue", ttype) 22 | } 23 | } 24 | 25 | // ModQueueAfter returns new modqueue entries from a subreddit 26 | // 27 | // # Last is the anchor of a modqueue entry id 28 | // 29 | // Limit is any numerical value, so 0 <= limit <= 100. 30 | func (c *Reddit) ModQueueAfter(last string, limit int) ([]models.ModQueueListingChild, error) { 31 | name, ttype := c.getQueue() 32 | switch ttype { 33 | case subredditType: 34 | return c.getSubredditModQueueAfter(name, last, limit) 35 | default: 36 | return nil, fmt.Errorf("'%s' type does not have an option for modqueue", ttype) 37 | } 38 | } 39 | 40 | func (c *Reddit) getSubredditModQueue(sr string, limit int) ([]models.ModQueueListingChild, error) { 41 | target := RedditOauth + "/r/" + sr + "/about/modqueue.json" 42 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 43 | "limit": strconv.Itoa(limit), 44 | }) 45 | if err != nil { 46 | return nil, errors.Wrap(err, "mira request failed in getSubredditModQueue") 47 | } 48 | ret := models.ModQueueListing{} 49 | err = json.Unmarshal(ans, &ret) 50 | return ret.GetChildren(), err 51 | } 52 | 53 | func (c *Reddit) getSubredditModQueueAfter(sr string, last string, limit int) ([]models.ModQueueListingChild, error) { 54 | target := RedditOauth + "/r/" + sr + "/about/modqueue.json" 55 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 56 | "limit": strconv.Itoa(limit), 57 | "before": last, 58 | }) 59 | if err != nil { 60 | return nil, errors.Wrap(err, "mira request failed in getSubredditModQueueAfter") 61 | } 62 | ret := models.ModQueueListing{} 63 | err = json.Unmarshal(ans, &ret) 64 | return ret.GetChildren(), err 65 | } 66 | -------------------------------------------------------------------------------- /reddit.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/thecsw/mira/v4/models" 12 | ) 13 | 14 | // MiraRequest Reddit API is always developing and I can't implement all endpoints; 15 | // It will be a bit of a bloat; Instead, you have accessto *Reddit.MiraRequest 16 | // method that will let you to do any custom reddit api calls! 17 | // 18 | // Here is the signature: 19 | // 20 | // func (c *Reddit) MiraRequest(method string, target string, payload map[string]string) ([]byte, error) {...} 21 | // 22 | // It is pretty straight-forward, the return is a slice of bytes; Parse it yourself. 23 | func (c *Reddit) MiraRequest(method string, target string, payload map[string]string) ([]byte, error) { 24 | values := "?" 25 | for i, v := range payload { 26 | v = url.QueryEscape(v) 27 | values += fmt.Sprintf("%s=%s&", i, v) 28 | } 29 | values = values[:len(values)-1] 30 | r, err := http.NewRequest(method, target+values, nil) 31 | if err != nil { 32 | return nil, err 33 | } 34 | r.Header.Set("User-Agent", c.Creds.UserAgent) 35 | r.Header.Set("Authorization", "Bearer "+c.Token) 36 | response, err := c.Client.Do(r) 37 | if err != nil { 38 | return nil, err 39 | } 40 | defer response.Body.Close() 41 | buf := new(bytes.Buffer) 42 | buf.ReadFrom(response.Body) 43 | data := buf.Bytes() 44 | if err := findRedditError(data); err != nil { 45 | return nil, err 46 | } 47 | return data, nil 48 | } 49 | 50 | // Me pushes a new Redditor value. 51 | func (c *Reddit) Me() *Reddit { 52 | return c.addQueue(c.Creds.Username, meType) 53 | } 54 | 55 | // Subreddit pushes a new subreddit value to the internal queue. 56 | func (c *Reddit) Subreddit(name ...string) *Reddit { 57 | return c.addQueue(strings.Join(name, "+"), subredditType) 58 | } 59 | 60 | // Submission pushes a new submission value to the internal queue. 61 | func (c *Reddit) Submission(name string) *Reddit { 62 | return c.addQueue(name, submissionType) 63 | } 64 | 65 | // Comment pushes a new comment value to the internal queue. 66 | func (c *Reddit) Comment(name string) *Reddit { 67 | return c.addQueue(name, commentType) 68 | } 69 | 70 | // Redditor pushes a new redditor value to the internal queue. 71 | func (c *Reddit) Redditor(name string) *Reddit { 72 | return c.addQueue(name, redditorType) 73 | } 74 | 75 | // Info returns MiraInterface of last pushed object. 76 | func (c *Reddit) Info() (MiraInterface, error) { 77 | name, ttype := c.getQueue() 78 | switch ttype { 79 | case meType: 80 | return c.getMe() 81 | case submissionType: 82 | return c.getSubmission(name) 83 | case commentType: 84 | return c.getComment(name) 85 | case subredditType: 86 | return c.getSubreddit(name) 87 | case redditorType: 88 | return c.getUser(name) 89 | default: 90 | return nil, fmt.Errorf("returning type is not defined") 91 | } 92 | } 93 | 94 | func (c *Reddit) getMe() (models.Me, error) { 95 | target := RedditOauth + "/api/v1/me" 96 | ret := &models.Me{} 97 | ans, err := c.MiraRequest("GET", target, nil) 98 | if err != nil { 99 | return *ret, err 100 | } 101 | json.Unmarshal(ans, ret) 102 | return *ret, nil 103 | } 104 | 105 | func (c *Reddit) getSubmission(id string) (models.PostListingChild, error) { 106 | target := RedditOauth + "/api/info.json" 107 | ans, err := c.MiraRequest("GET", target, map[string]string{ 108 | "id": id, 109 | }) 110 | ret := &models.PostListing{} 111 | json.Unmarshal(ans, ret) 112 | if len(ret.GetChildren()) < 1 { 113 | return models.PostListingChild{}, fmt.Errorf("id not found") 114 | } 115 | return ret.GetChildren()[0], err 116 | } 117 | 118 | func (c *Reddit) getUser(name string) (models.Redditor, error) { 119 | target := RedditOauth + "/user/" + name + "/about" 120 | ans, err := c.MiraRequest(http.MethodGet, target, nil) 121 | ret := &models.Redditor{} 122 | json.Unmarshal(ans, ret) 123 | return *ret, err 124 | } 125 | 126 | func (c *Reddit) getSubreddit(name string) (models.Subreddit, error) { 127 | target := RedditOauth + "/r/" + name + "/about" 128 | ans, err := c.MiraRequest(http.MethodGet, target, nil) 129 | ret := &models.Subreddit{} 130 | json.Unmarshal(ans, ret) 131 | return *ret, err 132 | } 133 | -------------------------------------------------------------------------------- /reddit_interface.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | // MiraInterface is the interface that unites 4 | // most of the reddit objects that mira uses 5 | // and exposes some of the most used methods 6 | type MiraInterface interface { 7 | GetId() string 8 | GetParentId() string 9 | GetTitle() string 10 | GetBody() string 11 | GetAuthor() string 12 | GetName() string 13 | GetKarma() float64 14 | GetUps() float64 15 | GetDowns() float64 16 | GetSubreddit() string 17 | GetCreated() float64 18 | GetFlair() string 19 | GetUrl() string 20 | IsRoot() bool 21 | } 22 | -------------------------------------------------------------------------------- /reddit_struct.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // Reddit is the main mira struct that practically 9 | // does everything 10 | type Reddit struct { 11 | Token string `json:"access_token"` 12 | Duration float64 `json:"expires_in"` 13 | Creds Credentials 14 | Chain chan *ChainVals 15 | Stream Streaming 16 | Values RedditVals 17 | Client *http.Client 18 | } 19 | 20 | // Streaming is used for some durations on how frequently 21 | // do we listen to comments/submissions 22 | type Streaming struct { 23 | CommentListInterval time.Duration 24 | PostListInterval time.Duration 25 | PostListSlice int 26 | ReportsInterval time.Duration 27 | ModQueueInterval time.Duration 28 | } 29 | 30 | // RedditVals is just some values to backoff 31 | type RedditVals struct { 32 | GetSubmissionFromCommentTries int 33 | } 34 | 35 | // ChainVals is our queue values 36 | type ChainVals struct { 37 | Name string 38 | Type string 39 | } 40 | -------------------------------------------------------------------------------- /reports.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/pkg/errors" 10 | 11 | "github.com/thecsw/mira/v4/models" 12 | ) 13 | 14 | // Reports returns report entries from a subreddit up to a specified limit sorted by the given parameters 15 | // Limit is any numerical value, so 0 <= limit <= 100. 16 | func (c *Reddit) Reports(limit int) ([]models.ReportListingChild, error) { 17 | name, ttype := c.getQueue() 18 | switch ttype { 19 | case subredditType: 20 | return c.getSubredditReports(name, limit) 21 | default: 22 | return nil, fmt.Errorf("'%s' type does not have an option for reports", ttype) 23 | } 24 | } 25 | 26 | // ReportsAfter returns new report entries from a subreddit 27 | // 28 | // # Last is the anchor of a modqueue entry id 29 | // 30 | // Limit is any numerical value, so 0 <= limit <= 100. 31 | func (c *Reddit) ReportsAfter(last string, limit int) ([]models.ReportListingChild, error) { 32 | name, ttype := c.getQueue() 33 | switch ttype { 34 | case subredditType: 35 | return c.getSubredditReportsAfter(name, last, limit) 36 | default: 37 | return nil, fmt.Errorf("'%s' type does not have an option for reports", ttype) 38 | } 39 | } 40 | 41 | func unMarshalReports(ans []byte, mql models.ReportListing) (models.ReportListing, error) { 42 | err := json.Unmarshal(ans, &mql) 43 | return mql, err 44 | } 45 | 46 | func (c *Reddit) getSubredditReports(sr string, limit int) ([]models.ReportListingChild, error) { 47 | target := RedditOauth + "/r/" + sr + "/about/reports.json" 48 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 49 | "limit": strconv.Itoa(limit), 50 | }) 51 | if err != nil { 52 | return nil, errors.Wrap(err, "mira request failed in getSubredditReports") 53 | } 54 | ret := models.ReportListing{} 55 | ret, err = unMarshalReports(ans, ret) 56 | return ret.GetChildren(), err 57 | } 58 | 59 | func (c *Reddit) getSubredditReportsAfter(sr string, last string, limit int) ([]models.ReportListingChild, error) { 60 | target := RedditOauth + "/r/" + sr + "/about/reports.json" 61 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 62 | "limit": strconv.Itoa(limit), 63 | "before": last, 64 | }) 65 | if err != nil { 66 | return nil, errors.Wrap(err, "mira request failed in getSubredditReportsAfter") 67 | } 68 | ret := models.ReportListing{} 69 | ret, err = unMarshalReports(ans, ret) 70 | return ret.GetChildren(), err 71 | } 72 | -------------------------------------------------------------------------------- /streaming.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/thecsw/mira/v4/models" 7 | ) 8 | 9 | // StreamCommentReplies streams comment replies 10 | // c is the channel with all unread messages 11 | func (c *Reddit) StreamCommentReplies() <-chan models.Comment { 12 | ret := make(chan models.Comment, 100) 13 | go func() { 14 | for { 15 | un, _ := c.Me().ListUnreadMessages() 16 | for _, v := range un { 17 | if v.IsCommentReply() { 18 | // Only process comment replies and 19 | // mark them as read. 20 | ret <- v 21 | // You can read the message with 22 | c.Me().ReadMessage(v.GetId()) 23 | } 24 | } 25 | time.Sleep(c.Stream.CommentListInterval * time.Second) 26 | } 27 | }() 28 | return ret 29 | } 30 | 31 | // StreamMentions streams recent mentions 32 | // c is the channel with all unread messages 33 | func (c *Reddit) StreamMentions() <-chan models.Comment { 34 | ret := make(chan models.Comment, 100) 35 | go func() { 36 | for { 37 | un, _ := c.Me().ListUnreadMessages() 38 | for _, v := range un { 39 | if v.IsMention() { 40 | // Only process comment replies and 41 | // mark them as read. 42 | ret <- v 43 | // You can read the message with 44 | c.Me().ReadMessage(v.GetId()) 45 | } 46 | } 47 | time.Sleep(c.Stream.CommentListInterval * time.Second) 48 | } 49 | }() 50 | return ret 51 | } 52 | 53 | // StreamComments streams comments from a redditor or a subreddit 54 | // c is the channel with all comments 55 | func (c *Reddit) StreamComments() (<-chan models.Comment, error) { 56 | name, ttype, err := c.checkType(subredditType, redditorType) 57 | if err != nil { 58 | return nil, err 59 | } 60 | switch ttype { 61 | case subredditType: 62 | return c.streamSubredditComments(name) 63 | case redditorType: 64 | return c.streamRedditorComments(name) 65 | } 66 | return nil, nil 67 | } 68 | 69 | // StreamSubmissions streams submissions from a redditor or a subreddit 70 | // c is the channel with all submissions. 71 | func (c *Reddit) StreamSubmissions() (<-chan models.PostListingChild, error) { 72 | name, ttype, err := c.checkType(subredditType, redditorType) 73 | if err != nil { 74 | return nil, err 75 | } 76 | switch ttype { 77 | case subredditType: 78 | return c.streamSubredditSubmissions(name) 79 | case redditorType: 80 | return c.streamRedditorSubmissions(name) 81 | } 82 | return nil, nil 83 | } 84 | 85 | // StreamModQueue streams modqueue entries from a subreddit 86 | // c is the channel with all modqueue entries. 87 | func (c *Reddit) StreamModQueue() (<-chan models.ModQueueListingChild, error) { 88 | name, ttype, err := c.checkType(subredditType) 89 | if err != nil { 90 | return nil, err 91 | } 92 | switch ttype { 93 | case subredditType: 94 | return c.streamSubredditModQueue(name) 95 | } 96 | return nil, nil 97 | } 98 | 99 | func (c *Reddit) streamSubredditModQueue(subreddit string) (<-chan models.ModQueueListingChild, error) { 100 | ret := make(chan models.ModQueueListingChild, 100) 101 | anchor, err := c.Subreddit(subreddit).ModQueue(1) 102 | if err != nil { 103 | return nil, err 104 | } 105 | last := "" 106 | if len(anchor) > 0 { 107 | last = anchor[0].GetId() 108 | } 109 | go func() { 110 | for { 111 | new, _ := c.Subreddit(subreddit).ModQueueAfter(last, c.Stream.PostListSlice) 112 | if len(new) < 1 { 113 | time.Sleep(c.Stream.ModQueueInterval * time.Second) 114 | continue 115 | } 116 | last = new[0].GetId() 117 | for i := range new { 118 | ret <- new[len(new)-i-1] 119 | } 120 | time.Sleep(c.Stream.ModQueueInterval * time.Second) 121 | } 122 | }() 123 | return ret, nil 124 | } 125 | 126 | // StreamReports streams reports entries from a subreddit 127 | // c is the channel with all report entries. 128 | func (c *Reddit) StreamReports() (<-chan models.ReportListingChild, error) { 129 | name, ttype, err := c.checkType(subredditType) 130 | if err != nil { 131 | return nil, err 132 | } 133 | switch ttype { 134 | case subredditType: 135 | return c.streamSubredditReports(name) 136 | } 137 | return nil, nil 138 | } 139 | 140 | func (c *Reddit) streamSubredditReports(subreddit string) (<-chan models.ReportListingChild, error) { 141 | ret := make(chan models.ReportListingChild, 100) 142 | anchor, err := c.Subreddit(subreddit).Reports(1) 143 | if err != nil { 144 | return nil, err 145 | } 146 | last := "" 147 | if len(anchor) > 0 { 148 | last = anchor[0].GetId() 149 | } 150 | go func() { 151 | for { 152 | new, _ := c.Subreddit(subreddit).ReportsAfter(last, c.Stream.PostListSlice) 153 | if len(new) < 1 { 154 | time.Sleep(c.Stream.ReportsInterval * time.Second) 155 | continue 156 | } 157 | last = new[0].GetId() 158 | for i := range new { 159 | ret <- new[len(new)-i-1] 160 | } 161 | time.Sleep(c.Stream.ReportsInterval * time.Second) 162 | } 163 | }() 164 | return ret, nil 165 | } 166 | 167 | func (c *Reddit) streamSubredditComments(subreddit string) (<-chan models.Comment, error) { 168 | ret := make(chan models.Comment, 100) 169 | anchor, err := c.Subreddit(subreddit).Comments(New, Hour, 1) 170 | if err != nil { 171 | return nil, err 172 | } 173 | last := "" 174 | if len(anchor) > 0 { 175 | last = anchor[0].GetId() 176 | } 177 | go func() { 178 | for { 179 | un, _ := c.Subreddit(subreddit).CommentsAfter(New, last, 100) 180 | if len(un) < 1 { 181 | time.Sleep(c.Stream.CommentListInterval * time.Second) 182 | continue 183 | } 184 | last = un[0].GetId() 185 | for _, v := range un { 186 | ret <- v 187 | } 188 | time.Sleep(c.Stream.CommentListInterval * time.Second) 189 | } 190 | }() 191 | return ret, nil 192 | } 193 | 194 | func (c *Reddit) streamRedditorComments(redditor string) (<-chan models.Comment, error) { 195 | ret := make(chan models.Comment, 100) 196 | anchor, err := c.Redditor(redditor).Comments(New, Hour, 1) 197 | if err != nil { 198 | return nil, err 199 | } 200 | last := "" 201 | if len(anchor) > 0 { 202 | last = anchor[0].GetId() 203 | } 204 | go func() { 205 | for { 206 | un, _ := c.Redditor(redditor).CommentsAfter(New, last, 100) 207 | if len(un) < 1 { 208 | time.Sleep(c.Stream.CommentListInterval * time.Second) 209 | continue 210 | } 211 | last = un[0].GetId() 212 | for _, v := range un { 213 | ret <- v 214 | } 215 | time.Sleep(c.Stream.CommentListInterval * time.Second) 216 | } 217 | }() 218 | return ret, nil 219 | } 220 | 221 | func (c *Reddit) streamSubredditSubmissions(subreddit string) (<-chan models.PostListingChild, error) { 222 | ret := make(chan models.PostListingChild, 100) 223 | anchor, err := c.Subreddit(subreddit).Submissions(New, Hour, 1) 224 | if err != nil { 225 | return nil, err 226 | } 227 | last := "" 228 | if len(anchor) > 0 { 229 | last = anchor[0].GetId() 230 | } 231 | go func() { 232 | for { 233 | new, _ := c.Subreddit(subreddit).SubmissionsAfter(last, c.Stream.PostListSlice) 234 | if len(new) < 1 { 235 | time.Sleep(c.Stream.PostListInterval * time.Second) 236 | continue 237 | } 238 | last = new[0].GetId() 239 | for i := range new { 240 | ret <- new[len(new)-i-1] 241 | } 242 | time.Sleep(c.Stream.PostListInterval * time.Second) 243 | } 244 | }() 245 | return ret, nil 246 | } 247 | 248 | func (c *Reddit) streamRedditorSubmissions(redditor string) (<-chan models.PostListingChild, error) { 249 | ret := make(chan models.PostListingChild, 100) 250 | anchor, err := c.Redditor(redditor).Submissions(New, Hour, 1) 251 | if err != nil { 252 | return nil, err 253 | } 254 | last := "" 255 | if len(anchor) > 0 { 256 | last = anchor[0].GetId() 257 | } 258 | go func() { 259 | for { 260 | new, _ := c.Redditor(redditor).SubmissionsAfter(last, c.Stream.PostListSlice) 261 | if len(new) < 1 { 262 | time.Sleep(c.Stream.PostListInterval * time.Second) 263 | continue 264 | } 265 | last = new[0].GetId() 266 | for i := range new { 267 | ret <- new[len(new)-i-1] 268 | } 269 | time.Sleep(c.Stream.PostListInterval * time.Second) 270 | } 271 | }() 272 | return ret, nil 273 | } 274 | -------------------------------------------------------------------------------- /submissions.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "regexp" 9 | "strconv" 10 | 11 | "github.com/thecsw/mira/v4/models" 12 | ) 13 | 14 | // Submissions returns submissions from a subreddit up to a specified limit sorted by the given parameters 15 | // 16 | // Sorting options: `Hot`, `New`, `Top`, `Rising`, `Controversial`, `Random` 17 | // 18 | // Duration options: `Hour`, `Day`, `Week`, `Year`, `All` 19 | // 20 | // Limit is any numerical value, so 0 <= limit <= 100. 21 | func (c *Reddit) Submissions(sort string, tdur string, limit int) ([]models.PostListingChild, error) { 22 | name, ttype := c.getQueue() 23 | switch ttype { 24 | case subredditType: 25 | return c.getSubredditPosts(name, sort, tdur, limit) 26 | case redditorType: 27 | return c.getRedditorPosts(name, sort, tdur, limit) 28 | default: 29 | return nil, fmt.Errorf("'%s' type does not have an option for submissions", ttype) 30 | } 31 | } 32 | 33 | // SubmissionsAfter returns new submissions from a subreddit 34 | // 35 | // # Last is the anchor of a submission id 36 | // 37 | // Limit is any numerical value, so 0 <= limit <= 100. 38 | func (c *Reddit) SubmissionsAfter(last string, limit int) ([]models.PostListingChild, error) { 39 | name, ttype := c.getQueue() 40 | switch ttype { 41 | case subredditType: 42 | return c.getSubredditPostsAfter(name, last, limit) 43 | case redditorType: 44 | return c.getRedditorPostsAfter(name, last, limit) 45 | default: 46 | return nil, fmt.Errorf("'%s' type does not have an option for submissions", ttype) 47 | } 48 | } 49 | 50 | // ExtractSubmission extracts submission id from last pushed object 51 | // does not make an api call like .Root(), use this instead. 52 | func (c *Reddit) ExtractSubmission() (string, error) { 53 | name, _, err := c.checkType(commentType) 54 | if err != nil { 55 | return "", err 56 | } 57 | info, err := c.Comment(name).Info() 58 | if err != nil { 59 | return "", err 60 | } 61 | link := info.GetUrl() 62 | reg := regexp.MustCompile(`comments/([^/]+)/`) 63 | res := reg.FindStringSubmatch(link) 64 | if len(res) < 1 { 65 | return "", errors.New("couldn't extract submission id") 66 | } 67 | return "t3_" + res[1], nil 68 | } 69 | 70 | // Root will return the submission id of a comment 71 | // Very expensive on API calls, please use .ExtractSubmission() instead. 72 | func (c *Reddit) Root() (string, error) { 73 | name, _, err := c.checkType(commentType) 74 | if err != nil { 75 | return "", err 76 | } 77 | current := name 78 | // Not a comment passed 79 | if string(current[1]) != "1" { 80 | return "", errors.New("the passed ID is not a comment") 81 | } 82 | target := RedditOauth + "/api/info.json" 83 | temp := models.CommentListing{} 84 | tries := 0 85 | for string(current[1]) != "3" { 86 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 87 | "id": current, 88 | }) 89 | if err != nil { 90 | return "", err 91 | } 92 | json.Unmarshal(ans, &temp) 93 | if len(temp.Data.Children) < 1 { 94 | return "", errors.New("could not find the requested comment") 95 | } 96 | current = temp.Data.Children[0].GetParentId() 97 | tries++ 98 | if tries > c.Values.GetSubmissionFromCommentTries { 99 | return "", fmt.Errorf("exceeded the maximum number of iterations: %v", 100 | c.Values.GetSubmissionFromCommentTries) 101 | } 102 | } 103 | return current, nil 104 | } 105 | 106 | func (c *Reddit) getRedditorPosts(user string, sort string, tdur string, limit int) ([]models.PostListingChild, error) { 107 | target := RedditOauth + "/u/" + user + "/submitted/" + sort + ".json" 108 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 109 | "limit": strconv.Itoa(limit), 110 | "t": tdur, 111 | }) 112 | ret := &models.PostListing{} 113 | json.Unmarshal(ans, ret) 114 | return ret.GetChildren(), err 115 | } 116 | 117 | func (c *Reddit) getRedditorPostsAfter(user string, last string, limit int) ([]models.PostListingChild, error) { 118 | target := RedditOauth + "/u/" + user + "/submitted/new.json" 119 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 120 | "limit": strconv.Itoa(limit), 121 | "before": last, 122 | }) 123 | ret := &models.PostListing{} 124 | json.Unmarshal(ans, ret) 125 | return ret.GetChildren(), err 126 | } 127 | 128 | func (c *Reddit) getSubredditPosts(sr string, sort string, tdur string, limit int) ([]models.PostListingChild, error) { 129 | target := RedditOauth + "/r/" + sr + "/" + sort + ".json" 130 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 131 | "limit": strconv.Itoa(limit), 132 | "t": tdur, 133 | }) 134 | ret := &models.PostListing{} 135 | json.Unmarshal(ans, ret) 136 | return ret.GetChildren(), err 137 | } 138 | 139 | func (c *Reddit) getSubredditPostsAfter(sr string, last string, limit int) ([]models.PostListingChild, error) { 140 | target := RedditOauth + "/r/" + sr + "/new.json" 141 | ans, err := c.MiraRequest(http.MethodGet, target, map[string]string{ 142 | "limit": strconv.Itoa(limit), 143 | "before": last, 144 | }) 145 | ret := &models.PostListing{} 146 | json.Unmarshal(ans, ret) 147 | return ret.GetChildren(), err 148 | } 149 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package mira 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | // Short runes to variablize our types. 13 | const ( 14 | submissionType = "s" 15 | subredditType = "b" 16 | commentType = "c" 17 | redditorType = "r" 18 | meType = "m" 19 | ) 20 | 21 | func (c *Reddit) checkType(rtype ...string) (string, string, error) { 22 | name, ttype := c.getQueue() 23 | if name == "" { 24 | return "", "", fmt.Errorf("identifier is empty") 25 | } 26 | if !findElem(ttype, rtype) { 27 | return "", "", fmt.Errorf( 28 | "the passed type is not a valid type for this call | expected: %s", 29 | strings.Join(rtype, ", ")) 30 | } 31 | return name, ttype, nil 32 | } 33 | 34 | func (c *Reddit) addQueue(name string, ttype string) *Reddit { 35 | c.Chain <- &ChainVals{Name: name, Type: ttype} 36 | return c 37 | } 38 | 39 | func (c *Reddit) getQueue() (string, string) { 40 | if len(c.Chain) < 1 { 41 | return "", "" 42 | } 43 | temp := <-c.Chain 44 | return temp.Name, temp.Type 45 | } 46 | 47 | func findElem(elem string, arr []string) bool { 48 | for _, v := range arr { 49 | if elem == v { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | 56 | // RedditErr is a struct to store reddit error messages. 57 | type RedditErr struct { 58 | Message string `json:"message"` 59 | Error string `json:"error"` 60 | } 61 | 62 | func findRedditError(data []byte) error { 63 | object := &RedditErr{} 64 | json.Unmarshal(data, object) 65 | if object.Message != "" || object.Error != "" { 66 | return fmt.Errorf("%s | error code: %s", object.Message, object.Error) 67 | } 68 | return nil 69 | } 70 | 71 | // ReadCredsFromFile reads mira credentials from a given file path 72 | func ReadCredsFromFile(file string) Credentials { 73 | // Declare all regexes 74 | ClientID, _ := regexp.Compile(`CLIENT_ID\s*=\s*(.+)`) 75 | ClientSecret, _ := regexp.Compile(`CLIENT_SECRET\s*=\s*(.+)`) 76 | Username, _ := regexp.Compile(`USERNAME\s*=\s*(.+)`) 77 | Password, _ := regexp.Compile(`PASSWORD\s*=\s*(.+)`) 78 | UserAgent, _ := regexp.Compile(`USER_AGENT\s*=\s*(.+)`) 79 | data, err := ioutil.ReadFile(file) 80 | if err != nil { 81 | return Credentials{} 82 | } 83 | s := string(data) 84 | creds := Credentials{ 85 | ClientID.FindStringSubmatch(s)[1], 86 | ClientSecret.FindStringSubmatch(s)[1], 87 | Username.FindStringSubmatch(s)[1], 88 | Password.FindStringSubmatch(s)[1], 89 | UserAgent.FindStringSubmatch(s)[1], 90 | } 91 | return creds 92 | } 93 | 94 | // ReadCredsFromEnv reads mira credentials from environment 95 | func ReadCredsFromEnv() Credentials { 96 | return Credentials{ 97 | os.Getenv("BOT_CLIENT_ID"), 98 | os.Getenv("BOT_CLIENT_SECRET"), 99 | os.Getenv("BOT_USERNAME"), 100 | os.Getenv("BOT_PASSWORD"), 101 | os.Getenv("BOT_USER_AGENT"), 102 | } 103 | } 104 | --------------------------------------------------------------------------------