├── .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 | 
2 |
3 |
4 |
5 | [](https://goreportcard.com/report/github.com/thecsw/mira/v4)
6 | [](https://godoc.org/github.com/thecsw/mira/v4)
7 | [](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 | 
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 \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 \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'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'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're all nice people here.<\/p><\/li>\n<\/ol>\n\n<hr\/>\n\n<p> <\/p>\n\n<h4><sup>Please don'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 \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 |
--------------------------------------------------------------------------------