├── logs
└── latest.log
├── roboto-emoji.ttf
├── .idea
└── .gitignore
├── forever.sh
├── config.ini
├── messages.py
├── README.md
├── LICENSE
└── bot.py
/logs/latest.log:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/roboto-emoji.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CalicoCatalyst/titletoimagebot/HEAD/roboto-emoji.ttf
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /workspace.xml
3 |
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
--------------------------------------------------------------------------------
/forever.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | from subprocess import Popen
3 | import sys
4 |
5 | filename = sys.argv[1]
6 | while True:
7 | print("\nStarting " + filename)
8 | p = Popen("python3 bot.py -l -d 10 30", shell=True)
9 | p.wait()
10 |
--------------------------------------------------------------------------------
/config.ini:
--------------------------------------------------------------------------------
1 | [Title2ImageBot]
2 | maintainer=CalicoCatalyst
3 | [RedditAuth]
4 | username=Title2ImageBot
5 | password=
6 | publicKey=
7 | privateKey=
8 | userAgent=Fork of /u/TitleToImageBot which is now defunct :(
9 | [ImgurAuth]
10 | publicKey=
11 | [GfyCatAuth]
12 | username=Title2ImageBot
13 | password=
14 | publicKey=
15 | privateKey=
16 | [IgnoreList]
17 | AutoModerator = True
18 | ThesaurizeThisBot = True
19 | [MinimalList]
20 | circlejerk = True
21 | [BanList]
22 | Freefolk = True
23 | # Below are the subs to automatically parse.
24 | [boottoobig]
25 | threshold=5
26 | triggers = ,|;|roses
27 | [TitleToImageBotSpam]
28 |
--------------------------------------------------------------------------------
/messages.py:
--------------------------------------------------------------------------------
1 | """
2 | Created on Jan 25, 2019
3 |
4 | @author: calicocatalyst
5 | """
6 |
7 | standard_reply_template = '''[Image with added {custom}title]({image_url}) {nsfw}\n\n
8 | {upscaled}\n\n
9 | {warntag}\n\n
10 | ---\n\n
11 | ^Summon ^me ^with ^/u/title2imagebot ^or ^by ^PMing ^me ^a ^post ^with ^"parse" ^as ^the ^subject. ^|
12 | [^About](http://calicocat.live/t2ib) ^|
13 | [^feedback](https://reddit.com/message/compose/?to=CalicoCatalyst&subject=feedback%20{submission_id}) ^|
14 | [^source](https://github.com/calicocatalyst/titletoimagebot) ^|
15 | ^Fork ^of ^TitleToImageBot'''
16 |
17 |
18 | site19_template = '''[Image with [REDACTED] {custom}title]({image_url}) {nsfw}\n\n
19 | {upscaled}\n\n
20 | {warntag}\n\n
21 | ---\n\n
22 | ^Summon ^me ^with ^[REDACTED] ^|
23 | [^church ^of ^peanut](http://reddit.com/r/churchofpeanut) ^|
24 | [^Which ^SCP ^would ^you ^yiff ^and ^why ^is ^it ^1471](https://reddit.com/message/compose/?to=CalicoCatalyst&subject=feedback%20{submission_id}) ^|
25 | [^source](https://github.com/calicocatalyst/titletoimagebot) ^|
26 | ^Fork ^of ^[REDACTED]'''
27 |
28 |
29 |
30 | minimal_reply_template = '[Processed Image]({image_url}) {nsfw}'
31 |
32 | banned_PM_template = '''[Here is your image request]({image_url}) {nsfw}\n\n
33 | {upscaled}\n\n
34 | {warntag}\n\n
35 | ---\n\n
36 | Unfortunately, it looks like I'm banned in the sub I was summoned in.
37 | Feel free to post this link in the comment you summoned me in!.\n\n
38 | ---\n\n
39 | Summon me with /u/title2imagebot |
40 | [About](http://calico.live/t2ib) |
41 | [feedback](https://reddit.com/message/compose/?to=CalicoCatalyst&subject=feedback%20{submission_id}) |
42 | [source](https://github.com/calicocatalyst/titletoimagebot) |
43 | Fork of TitleToImageBot
44 | '''
45 |
46 | PM_reply_template = '''[Image with added {custom}title]({image_url}) {nsfw}\n\n
47 | {upscaled}\n\n
48 | {warntag}\n\n
49 | ---\n\n
50 | Summon me with /u/title2imagebot or by PM! |
51 | [About](http://calico.live/t2ib) |
52 | [feedback](https://reddit.com/message/compose/?to=CalicoCatalyst&subject=feedback%20{submission_id}) |
53 | [source](https://github.com/calicocatalyst/titletoimagebot) |
54 | Fork of TitleToImageBot
55 | '''
56 |
57 | de_reply_template = '''[Bild mit hinzugefügtem {custom}Titel]({image_url}) {nsfw}\n\n
58 | {upscaled}\n\n
59 | {warntag}\n\n
60 | ---\n\n
61 | ^Rufen ^Sie ^mich ^mit ^/u/Title2ImageBot ^an ^oder ^senden ^Sie ^mir ^einen ^Link ^zu ^einem ^Beitrag ^mit
62 | ^dem ^Betreff ^"parse". ^|
63 | [^Info](http://calico.live/t2ib) ^|
64 | [^Schrei ^mich ^an ^oder ^hilf ^mir ^zu ^übersetzen](https://reddit.com/message/compose/?to=CalicoCatalyst&subject=feedback) ^|
65 | [^Quellcode](https://github.com/calicocatalyst/titletoimagebot) ^|
66 | ^Git ^Fork ^aus ^TitleToImageBot'''
67 |
68 | gif_warning = "Gif processing is currently in alpha. There may be framerate issues if it does manage to parse"
69 |
70 | PM_options_warning = "PM processing is in beta and may not correctly process custom arguments"
71 |
72 | custom_args_warning = "Custom arguments are currently in alpha. They may not work correctly, and hopefully the bot " \
73 | "doesnt crash "
74 |
75 | comment_url = "https://reddit.com/comments/{postid}/_/{commentid}"
76 |
77 | already_responded_message = "Looks like I've already responded in this thread [Here!]({commentlink})"
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Title2ImageBot
2 |
3 | Rewrite of original bot that was written by gerenook. Reddit bot for adding contents of title to image.
4 |
5 | ## Features
6 |
7 | ### Standard Parsing:
8 | Tag the bot in a post with /u/Title2ImageBot. The bot will automatically respond within 30 seconds with the post's title included in the image.
9 |
10 | ### Custom Titles:
11 | Include a custom title in quotes like so:
12 |
13 | ```/u/Title2ImageBot "custom title goes here"```
14 |
15 | The bot will include the post with the custom title, and still allow users to use other custom titles or the original
16 |
17 | ### Other stuff
18 |
19 | Now responds to /u/titletoimagebot.
20 |
21 | ### Custom Arguments:
22 | Custom arguments can be used like so:
23 |
24 | ```/u/Title2ImageBot "custom title still goes here if needed" !center !dark```
25 |
26 | `!c, !center, !middle` will center the text at the top of the image
27 |
28 | `!d, !dark` will invert the title section (white text on black)
29 |
30 | `!a, !author, !tagauth, !tagauthor` will include the submitter's username in the image.
31 |
32 | It appears dark mode was an intended feature at some point in the original bot, so thanks to gerenook for some of the code required to make it work
33 |
34 | ### PM parsing:
35 | Bot can be used via PM if its banned in a sub (lots of subs thanks to /r/BotBust banning me twice)
36 |
37 | PM a link to a submission. Subject should be "parse"
38 |
39 | # Developer Stuff
40 |
41 | ### Code / IDE
42 |
43 | This project is written in Pycharm IDE by IntelliJ, and honestly has only been made possible via their Open Source Licensing program.
44 | It includes comments that note different things to the inspector. I would highly reccomend using this IDE if you intend on working on this
45 | project, or just in general as it makes working with python feel as scalable as working with more industrial
46 | programming languages.
47 |
48 | #### Docstrings
49 |
50 | Docstrings are written in Google's python docstring format
51 |
52 | ### Running the bot
53 |
54 | (Please dont try to run another bot unless /u/Title2ImageBot is shut down for good. Running it on your own sub is fine, but if you want DM me and I can set this one to automatically run on your sub)
55 |
56 | Depends:
57 |
58 | > Praw
59 | > Pillow
60 | > PyImgur
61 | > GfyPy
62 | > ImgurPython
63 | > argparse
64 |
65 | ```
66 | usage: bot.py [-h] [-d] [-l] limit interval
67 |
68 | Bot To Add Titles To Images
69 |
70 | positional arguments:
71 | limit amount of submissions/messages to process each cycle
72 | interval time (in seconds) to wait between cycles
73 |
74 | optional arguments:
75 | -h, --help show this help message and exit
76 | -d, --debug Enable Debug Logging
77 | -l, --loop Enable Looping Function
78 | ```
79 |
80 | ### Command Line Interface
81 |
82 | This puppy is my pride and joy, and tbh I've spent just as much time perfecting this as I have on the bot.
83 | The program interfaces with the `CLI()` class. Ctrl+C to end the curses session and start command line debugging.
84 | Ctrl+C again to quit
85 |
86 | ### Command Line Debugging
87 |
88 | Command line debugging is a feature I have admittedly not used at any point. Regardless, its cool
89 | to have. *This wont work with the forever.sh script due to it not handling things like a normal CLI.*
90 |
91 | Ctrl+C (KeyboardInterrupt) to:
92 |
93 | 1. Kill any active threads
94 | 2. End curses session
95 | 3. Set the killflag in `CLI()` to keep it from updating again
96 | 4. Start a `while True` loop that eval()'s `raw_input('>>> ')`
97 |
98 | type `quit` to kill everything, clear curses again (to be safe idk), run
99 | `stty sane; clear`, and `exit(0)`
100 |
101 | ### Logging
102 |
103 | Verbose live logging now gets saved to a file. current one writes to logs/latest.log, and on program start renames the last one with
104 | the current timestamp (e.g. `logfile-1555755107.9048734.log`). I would highly suggest passing the `-d` flag as it makes the logs at least somewhat
105 | useful.
106 |
107 | ### Loop function
108 |
109 | Most of the program is designed around the `-l` flag being passed. It's made to run as a looping bot. While it will definitely
110 | function without it, it's not written for it, so be aware of this.
111 |
112 | ## TODO:
113 | * Fix Gif framerate issues
114 | * Make everything a thread
115 | * Add more dumb trivial features
116 |
117 | # Credits
118 |
119 | /u/gerenook for original bot's code, most of the `RedditImage` class is his work.
120 |
121 | Roboto-Emoji is a custom font I created in FontForge that adds support for emojis.
122 | Feel free to use it.
123 |
124 | GfyPy is a gfycat python api I wrote with extremely basic support for what i needed. It's on my pinned repositories tab. Feel free to contribute as it's currently the only gfycat python api with auth support
125 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/bot.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Title2ImageBot
6 | Complete redesign of titletoimagebot with non-deprecated apis
7 |
8 | Always striving to improve this bot, fix bugs that crash it, and keep pushing forward with its design.
9 | Contributions welcome
10 |
11 | Written and maintained by CalicoCatalyst
12 |
13 | """
14 | import argparse
15 | import configparser
16 | import curses
17 | import logging
18 | import os
19 | import re
20 | import sqlite3
21 | import threading
22 | import time
23 | from io import BytesIO
24 | from math import ceil
25 | from os import remove
26 |
27 | import praw
28 | import praw.exceptions
29 | import praw.models
30 | import prawcore
31 | # Use my custom fork until https://github.com/Damgaard/PyImgur/pull/43 is merged.
32 | # pip3 install git+https://github.com/CalicoCatalyst/PyImgur
33 | import pyimgur
34 | import requests
35 | from PIL import Image, ImageSequence, ImageFont, ImageDraw
36 | from bs4 import BeautifulSoup
37 | from gfypy import gfycat
38 | # noinspection PyProtectedMember
39 | from pyimgur.request import ImgurException
40 | from requests import HTTPError
41 |
42 | import messages
43 |
44 | __author__ = 'calicocatalyst'
45 | # [Major, e.g. a complete source code refactor].[Minor e.g. a large amount of changes].[Feature].[Patch]
46 | __version__ = '1.1.3.0'
47 |
48 |
49 | class TitleToImageBot(object):
50 | """Class for the bot itself.
51 |
52 | Attributes:
53 | config (Configuration): Bot Configuration Object
54 | reddit (praw.Reddit): Reddit client object
55 | imgur (PyImgur.Imgur): Imgur client object
56 | gfycat (Gfycat.gfycat): Gfycat client object
57 | screen (CLI): CLI interface object
58 | killthreads (bool): Setting this to true will kill any active threads
59 | """
60 | def __init__(self, config, database, screen):
61 | """Create the bot object
62 |
63 | Args:
64 | config (Configuration): Bot configuration API object
65 | database (BotDatabase): Bot database API object
66 | screen (CLI): Bot CLI API object
67 | """
68 | # The conifugration has all of the usernames/passwords/keys.
69 | self.config = config
70 |
71 | # Ask for our API objects from the config
72 | self.reddit = self.config.auth_reddit_from_config()
73 | self.imgur = self.config.get_imgur_client_config()
74 | self.gfycat = self.config.get_gfycat_client_config()
75 | self.screen = screen
76 |
77 | # get our custom BotDatabase object
78 | self.database = database
79 | self.killthreads = False
80 | self.thread = None
81 |
82 | def call_checks(self, limit):
83 | """Call the functions that check mentions and subs for requests
84 |
85 | Args:
86 | limit (int): How many posts back should be checked.
87 | """
88 |
89 | # Set the curses (console) text
90 | self.screen.set_current_action_status('Checking Mentions', "")
91 | # Log it to the file
92 | logging.info("Checking Mentions")
93 | #######################
94 | # CHECK MENTIONS #
95 | #######################
96 | self.check_mentions_for_requests(limit)
97 |
98 | # Same stuff but for our listed auto-reply subs
99 | self.screen.set_current_action_status('Checking Autoreply Subs', "")
100 | logging.info("Checking Autoreply Subs")
101 |
102 | #######################
103 | # CHECK SUBS #
104 | #######################
105 | self.check_subs_for_posts(limit)
106 |
107 | def read_comment_stream_for_manual_mentions(self):
108 | """Read a comment stream to check for all mentions of the old titletoimagebot
109 |
110 | Returns:
111 |
112 | """
113 |
114 | #######################
115 | # START STREAM LOOP #
116 | #######################
117 | for comment in self.reddit.subreddit('all').stream.comments():
118 |
119 | if 'u/titletoimagebot' in comment.body.lower() and comment.author.name is not 'Title2ImageBot':
120 |
121 | #######################
122 | # PROCESS SUBMISS #
123 | #######################
124 | processed = self.process_submission(comment.submission, comment, None,
125 | dm=False, request_body=comment.body, customargs=[])
126 | if processed is not None:
127 | processed_url = processed[0]
128 | processed_submission = processed[1]
129 | processed_source_comment = processed[2]
130 | # processed_custom_title_exists = processed[3]
131 | #######################
132 | # REPLY #
133 | #######################
134 | self.reply_imgur_url(processed_url, processed_submission, processed_source_comment,
135 | None, customargs=[])
136 | else:
137 | pass
138 |
139 | if self.killthreads:
140 | break
141 |
142 | def start_comment_streaming_thread(self):
143 | """Start up the comment streaming thread
144 | """
145 | #######################
146 | # CALL STREAM MTHD #
147 | #######################
148 | self.screen.set_stream_status("Active")
149 | thread = threading.Thread(target=self.read_comment_stream_for_manual_mentions, args=())
150 | thread.daemon = True
151 | thread.start()
152 | self.thread = thread
153 | # self.screen.set_stream_status("Active")
154 |
155 | def stop_comment_streaming_thread(self):
156 | """Stop the comment streaming thread
157 |
158 | """
159 | self.killthreads = True
160 | self.screen.set_stream_status("Disconnected")
161 | # curses.echo()
162 | # curses.nocbreak()
163 | # curses.endwin()
164 |
165 | def check_mentions_for_requests(self, post_limit=10):
166 | """Check the bot inbox for username mentions / PMs
167 |
168 | Args:
169 | post_limit (int): How far back in the inbox should we look.
170 |
171 | """
172 | # A majority of this is for the progress bar
173 | iteration = 1
174 | # Start the progress bar before we make the request.
175 | line = CLI.get_progress_line(iteration, post_limit)
176 | self.screen.set_current_action_status("Checking Inbox for requests", line)
177 |
178 | #######################
179 | # START CHECK LOOP #
180 | #######################
181 | for message in self.reddit.inbox.all(limit=post_limit):
182 | # If we're on the first one, show the progress bar not moving so we dont go over 100%
183 | if iteration is 1:
184 | # Add an iteration to the progress bar
185 | iteration = iteration + 1
186 | # Get our "Line" aka progress bar
187 | line = CLI.get_progress_line(1, post_limit + 1)
188 | # Send the line to curses (live console) with the included action.
189 | self.screen.set_current_action_status("Checking Inbox for requests", line)
190 | else:
191 | iteration = iteration + 1
192 | line = CLI.get_progress_line(iteration, post_limit + 1)
193 | self.screen.set_current_action_status("Checking Inbox for requests", line)
194 |
195 | # This is the actual function
196 | # noinspection PyBroadException
197 | try:
198 | # Actually send the item in the inbox to the method to process it.
199 |
200 | #######################
201 | # PROCESS MSG #
202 | #######################
203 | self.process_message(message)
204 |
205 | except Exception as ex:
206 | # Broad catch to prevent freak cases from crashing program.
207 | logging.info("Could not process %s with exception %s" % (message.id, ex))
208 |
209 | def check_subs_for_posts(self, post_limit=25):
210 | """Check autoprocess subs for posts that meet set requirements for sub
211 |
212 | Args:
213 | post_limit: How far back in the sub should we check
214 |
215 | """
216 | # Get list of subs from the config
217 | subs = self.config.get_automatic_processing_subs()
218 |
219 | # Caluclate the total amount of posts to be parsed
220 | totalits = len(subs) * post_limit
221 | iters = 0
222 | for sub in subs:
223 | # Subreddit Object for API interaction
224 | subr = self.reddit.subreddit(sub)
225 |
226 | # Grab posts from /new in sub to check
227 | for post in subr.new(limit=post_limit):
228 | iters += 1
229 | line = CLI.get_progress_line(iters, totalits)
230 |
231 | # Update curses
232 | self.screen.set_current_action_status("Checking Subs for posts", line)
233 |
234 | # If we've already parsed, skip this post iteration.
235 | if self.database.submission_exists(post.id):
236 | continue
237 |
238 | title = post.title
239 |
240 | # does this sub have list of keywords in the title that trigger the bot on this sub
241 | has_triggers = self.config.configfile.has_option(sub, 'triggers')
242 | # does this sub have an upvote-before-parsing threshold
243 | has_threshold = self.config.configfile.has_option(sub, 'threshold')
244 |
245 | if has_triggers:
246 | # Get our list of triggers.
247 | triggers = str(self.config.configfile[sub]['triggers']).split('|')
248 | # Skip if the title doesnt have one, but mark it as parsed.
249 | if not any(t in title.lower() for t in triggers):
250 | logging.debug('Title %s doesnt appear to contain any of %s, adding to parsed and skipping'
251 | % (title, self.config.configfile[sub]["triggers"]))
252 | self.database.submission_insert(post.id, post.author.name, title, post.url)
253 | continue
254 | else:
255 | # No triggers so keep moving
256 | logging.debug('No triggers were defined for %s, not checking' % sub)
257 |
258 | if has_threshold:
259 | # Get the karma threshold
260 | threshold = int(self.config.configfile[sub]['threshold'])
261 | if post.score < threshold:
262 | logging.debug('Threshold not met, not adding to parsed, just ignoring')
263 | continue
264 | else:
265 | logging.debug('Threshold met, posting and adding to parsed')
266 | else:
267 | logging.debug('No threshold for %s, replying to everything :)' % sub)
268 |
269 | #######################
270 | # PROCESS SUBMISS #
271 | #######################
272 | processed = self.process_submission(post, None, None)
273 |
274 | if processed is not None:
275 | processed_url = processed[0]
276 | processed_submission = processed[1]
277 | processed_source_comment = processed[2]
278 | # processed_custom_title_exists = processed[3]
279 |
280 | #######################
281 | # REPLY #
282 | #######################
283 | self.reply_imgur_url(processed_url, processed_submission, processed_source_comment,
284 | None, customargs=None)
285 | else:
286 | if self.database.submission_exists(post.id):
287 | continue
288 | else:
289 | self.database.submission_insert(post.id, post.author.name, title, post.url)
290 | continue
291 | if sub == "TitleToImageBotSpam":
292 | for comment in processed[1].comments.list():
293 | if isinstance(comment, praw.models.MoreComments):
294 | # See praw docs on MoreComments
295 | continue
296 | if not comment or comment.author is None:
297 | # If the comment or comment author was deleted, skip it
298 | continue
299 | if comment.author.name == self.reddit.user.me().name and \
300 | "Image with added title" in comment.body:
301 | comment.mod.distinguish(sticky=True)
302 |
303 | if self.database.submission_exists(post.id):
304 | continue
305 | else:
306 | self.database.submission_insert(post.id, post.author.name, title, post.url)
307 |
308 | def process_message(self, message):
309 | """Process a detected username mention / DM
310 |
311 | Args:
312 | message (praw.models.Comment): Message to process
313 | """
314 | if not message.author:
315 | return
316 |
317 | message_author = message.author.name
318 | subject = message.subject.lower()
319 | body_original = message.body
320 | body = message.body.lower()
321 |
322 | # Check if this message was already parsed. If so, dont parse it.
323 | if self.database.message_exists(message.id):
324 | logging.debug("bot.process_message() Message %s Already Parsed, Returning", message.id)
325 | return
326 |
327 | # Respond to the SCP Bot that erroneously detects "SCP-2" in every post.
328 | # There are two.
329 | if (message_author.lower() == "the-paranoid-android") or (message_author.lower() == "the-noided-android"):
330 | message.reply("Thanks Marv")
331 | logging.debug("Thanking marv")
332 | self.database.message_insert(message.id, message_author, message.subject.lower(), body)
333 | return
334 |
335 | # Skip Messages Sent by Bot
336 | if message_author == self.reddit.user.me().name:
337 | logging.debug('Message was sent, returning')
338 | return
339 |
340 | # Live Management by Bot Maintainer
341 | if message_author.lower() == self.config.maintainer.lower():
342 | if "!eval" in body:
343 | eval(body[5:])
344 | if "!del" in body or "!delete" in body:
345 | message.parent().delete()
346 | if "!edit" in body:
347 | message.parent().edit(body[5:])
348 | if "!append" in body:
349 | message.parent().edit(message.parent().body + body[7:])
350 |
351 | # Process the typical username mention
352 | if (isinstance(message, praw.models.Comment) and
353 | (subject == 'username mention' or
354 | (subject == 'comment reply' and 'u/%s' % (self.config.bot_username.lower()) in body))):
355 |
356 | if message.author.name.lower() == 'automoderator':
357 | message.mark_read()
358 | return
359 |
360 | match = re.match(r'.*u/%s\s*["“”](.+)["“”].*' % (self.config.bot_username.lower()),
361 | body_original, re.RegexFlag.IGNORECASE)
362 | title = None
363 | if match:
364 | title = match.group(1)
365 | if len(title) > 512:
366 | title = None
367 | else:
368 | logging.debug('Found custom title: %s', title)
369 |
370 | if message.submission.subreddit.display_name not in self.config.get_automatic_processing_subs() and \
371 | body is not None:
372 | customargs = []
373 |
374 | dark_mode_triggers = ["!dark", "!darkmode", "!black", "!d"]
375 | center_mode_triggers = ["!center", "!middle", "!c"]
376 | auth_tag_triggers = ["!author", "tagauthor", "tagauth", "!a"]
377 |
378 | # If we find any apparent commands include them
379 | if any(x in body for x in dark_mode_triggers):
380 | customargs.append("dark")
381 | if any(x in body for x in center_mode_triggers):
382 | customargs.append("center")
383 | if any(x in body for x in auth_tag_triggers):
384 | customargs.append("tagauth")
385 | else:
386 | customargs = []
387 |
388 | #######################
389 | # PROCESS SUBMIS. #
390 | #######################
391 | processed = self.process_submission(message.submission, message, title,
392 | dm=False, request_body=body, customargs=customargs)
393 | if processed is not None:
394 | processed_url = processed[0]
395 | processed_submission = processed[1]
396 | processed_source_comment = processed[2]
397 | # processed_custom_title_exists = processed[3]
398 |
399 | #######################
400 | # REPLY #
401 | #######################
402 | self.reply_imgur_url(processed_url, processed_submission, processed_source_comment,
403 | title, customargs=customargs)
404 | else:
405 | pass
406 |
407 | message.mark_read()
408 |
409 | # Process feedback and send it to bot maintainer
410 | elif subject.startswith('feedback'):
411 | self.reddit.redditor(self.config.maintainer).message("Feedback from %s" % message_author, body)
412 | # mark short good/bad bot comments as read to keep inbox clean
413 | elif 'good bot' in body and len(body) < 12:
414 | logging.debug('Good bot message or comment reply found, marking as read')
415 | message.mark_read()
416 | elif 'bad bot' in body and len(body) < 12:
417 | logging.debug('Bad bot message or comment reply found, marking as read')
418 | message.mark_read()
419 |
420 | # BETA Private Messaging Parsing feature
421 |
422 | pm_process_triggers = ["add", "parse", "title", "image"]
423 |
424 | if any(x in subject for x in pm_process_triggers):
425 | re1 = '.*?' # Non-greedy match on filler
426 | re2 = '((?:http|https)(?::\\/{2}[\\w]+)(?:[\\/|\\.]?)(?:[^\\s"]*))' # HTTP URL 1
427 |
428 | rg = re.compile(re1 + re2, re.IGNORECASE | re.DOTALL)
429 | m = rg.search(body)
430 | if m:
431 | http_url = m.group(1)
432 | else:
433 | return
434 | submission = self.reddit.submission(url=http_url)
435 |
436 | match = re.match(r'.*%s\s*["“”](.+)["“”].*' % http_url,
437 | body_original, re.RegexFlag.IGNORECASE)
438 | title = None
439 | if match:
440 | title = match.group(1)
441 | if len(title) > 512:
442 | title = None
443 | else:
444 | logging.debug('Found custom title: %s', title)
445 |
446 | #######################
447 | # PROCESS PM SUBM #
448 | #######################
449 | parsed = self.process_submission(submission, None, title, True, request_body=body_original)
450 |
451 | processed = parsed
452 | if processed is not None:
453 | processed_url = processed[0]
454 | # noinspection PyUnusedLocal
455 | processed_submission = processed[1]
456 | # noinspection PyUnusedLocal
457 | processed_source_comment = processed[2]
458 | processed_custom_title_exists = processed[3]
459 | custom_title = processed_custom_title_exists
460 | upscaled = False
461 | else:
462 | #######################
463 | # FALLBACK REPLIES #
464 | #######################
465 | self.reddit.redditor(message_author).message("Sorry, I wasn't able to process that. This feature is in"
466 | "beta and the conversation has been forwarded to the bot"
467 | "author to see if a fix is possible.")
468 | self.reddit.redditor(self.config.maintainer).message("Failed to process DM request. Plz investigate")
469 | return
470 | comment = messages.PM_reply_template.format(
471 | image_url=processed_url,
472 | warntag="PM Processing is in beta!",
473 | custom="custom " if custom_title else "",
474 | nsfw="(NSFW)" if submission.over_18 else '',
475 | upscaled=' (image was upscaled)\n\n' if upscaled else '',
476 | submission_id=submission.id
477 | )
478 | #######################
479 | # REPLY TO USER #
480 | #######################
481 | self.reddit.redditor(message_author).message('Re: ' + subject, comment)
482 | message.mark_read()
483 |
484 | # Check if the bot has processed already, if so we dont need to do anything. If it hasn't,
485 | # add it to the database and move on
486 | if self.database.message_exists(message.id):
487 | logging.debug("bot.process_message() Message %s Already Parsed, no need to add", message.id)
488 | return
489 | else:
490 | self.database.message_insert(message.id, message_author, subject, body)
491 |
492 | # noinspection PyUnusedLocal
493 | def process_submission(self, submission, source_comment, title, dm=None, request_body=None, customargs=None):
494 | """Send info to process_image_submission and handle errors that arise from that method.
495 |
496 | Args:
497 | submission (praw.models.Submission): Post to process
498 | source_comment (praw.models.Comment): Comment that summoned bot
499 | title (str): Title to add to the image
500 | dm (Optional[Any]): Unused variable
501 | request_body (str): Unusued; Body of the request
502 | customargs (list[str]): Custom arguments
503 | """
504 |
505 | #######################
506 | # MAIN FUNCTION #
507 | #######################
508 | url = self.process_image_submission(submission=submission, custom_title=title, customargs=customargs)
509 |
510 | #######################
511 | # DATABASE CHECKS #
512 | #######################
513 | if url is None:
514 |
515 | self.screen.set_current_action_status('URL returned as none.', "")
516 | logging.debug('Checking if Bot Has Already Processed Submission')
517 | # This should return if the bot has already replied.
518 | for comment in submission.comments.list():
519 | if isinstance(comment, praw.models.MoreComments):
520 | # See praw docs on MoreComments
521 | continue
522 | if not comment or comment.author is None:
523 | # If the comment or comment author was deleted, skip it
524 | continue
525 | if comment.author.name == self.reddit.user.me().name and 'Image with added title' in comment.body:
526 | if source_comment:
527 | self.redirect_to_comment(source_comment, comment, submission)
528 |
529 | # If there is no comment (automatic sub parsing) and the post wasn't deleted, and its not in the table, put
530 | # it in. This was a very specific issue and I'm not sure what the exact problem was, but this fixes it :)
531 | if (source_comment is None and
532 | submission is not None and
533 | not self.database.submission_exists(submission.id)):
534 | self.database.submission_insert(submission.id, submission.author.name, submission.title,
535 | submission.url)
536 | return
537 | # Dont parse if it's already been parsed
538 | if self.database.message_exists(source_comment.id):
539 | return
540 | else:
541 | self.database.message_insert(source_comment.id, source_comment.author.name, "comment reply",
542 | source_comment.body)
543 | return
544 |
545 | ######################
546 | # RETURN #
547 | ######################
548 | custom_title_exists = True if title is not None else False
549 | return [url, submission, source_comment, custom_title_exists]
550 |
551 | def redirect_to_comment(self, source_comment, comment, submission):
552 | """If a user isn't the first to ask for the bot to process, redirect them to the first asker
553 |
554 | Args:
555 | source_comment (praw.models.Comment): Comment that is currently asking
556 | comment (praw.models.Comment): Comment to redirect said user to
557 | submission (praw.models.Submission): Submission the post is on (for link generation purposes)
558 |
559 | """
560 | com_url = messages.comment_url.format(postid=submission.id, commentid=comment.id)
561 | reply = messages.already_responded_message.format(commentlink=com_url)
562 |
563 | try:
564 | #######################
565 | # REPLY TO USER #
566 | #######################
567 | source_comment.reply(reply)
568 | except prawcore.exceptions.Forbidden:
569 | try:
570 | source_comment.reply(reply)
571 | except prawcore.exceptions.Forbidden:
572 | logging.error("Failed to redirect user to comment")
573 | except praw.exceptions.APIException:
574 | logging.error("Failed to redirect user because user's comment was deleted")
575 | except Exception as ex:
576 | logging.critical("Failed to redirect user to comment with %s" % ex)
577 | self.database.message_insert(source_comment.id, comment.author.name, "comment reply", source_comment.body)
578 |
579 | # noinspection PyUnusedLocal
580 | def process_image_submission(self, submission, custom_title=None, commenter=None, customargs=None):
581 | """Process an image submission
582 |
583 | Args:
584 | submission (praw.models.Submission): Submission to process
585 | custom_title (str): Custom title to be potentially added
586 | commenter (str): Name of the person who commented. I dont think this is ever used
587 | customargs (list[str]): Custom arguments to process
588 |
589 | Returns:
590 | Imgur URL
591 | """
592 | if customargs:
593 | pls = ''.join(customargs)
594 | else:
595 | pls = ""
596 |
597 | if custom_title:
598 | parsed = self.database.submission_exists(submission.id + custom_title + pls)
599 | else:
600 | parsed = self.database.submission_exists(submission.id + pls)
601 |
602 | subreddit = submission.subreddit.display_name
603 |
604 | if parsed:
605 | #######################
606 | # BAIL #
607 | #######################
608 | return None
609 |
610 | # Make sure author account exists
611 | if submission.author is None:
612 | self.database.submission_insert(submission.id, "deletedPost", submission.title, submission.url)
613 | #######################
614 | # BAIL #
615 | #######################
616 | return None
617 |
618 | sub = submission.subreddit.display_name
619 | url = submission.url
620 | if custom_title is not None:
621 | title = custom_title
622 | else:
623 | title = submission.title
624 | submission_author = submission.author.name
625 |
626 | # We need to verify everything is good to go
627 | # Check every item in this list and verify it is 'True'
628 | # If the submission has been parsed, throw false which will not allow the Bot
629 | # To post.
630 |
631 | if parsed:
632 | #######################
633 | # BAIL #
634 | #######################
635 | return None
636 |
637 | if url.endswith('.gif') or url.endswith('.gifv'):
638 | # Lets try this again.
639 | # noinspection PyBroadException
640 | try:
641 |
642 | #######################
643 | # PROCESS GIFS #
644 | #######################
645 | return self.process_gif(submission)
646 | except Exception as ex:
647 | logging.warning("gif upload failed with %s" % ex)
648 | #######################
649 | # BAIL #
650 | #######################
651 | return None
652 |
653 | # Attempt to grab the images
654 | try:
655 | response = requests.get(url)
656 | img = Image.open(BytesIO(response.content))
657 | except (OSError, IOError) as error:
658 | logging.warning('Converting to image failed, trying with .jpg | %s', error)
659 | try:
660 | response = requests.get(url + '.jpg')
661 | img = Image.open(BytesIO(response.content))
662 | except (OSError, IOError) as error:
663 | logging.error('Converting to image failed, skipping submission | %s', error)
664 | #######################
665 | # BAIL #
666 | #######################
667 | return None
668 | except Exception as error:
669 | logging.error(error)
670 | logging.error('Exception on image conversion lines.')
671 | #######################
672 | # BAIL #
673 | #######################
674 | return None
675 | # noinspection PyBroadException
676 | try:
677 | image = RedditImage(img)
678 | except Exception as error:
679 | logging.error('Could not create RedditImage with %s' % error)
680 | #######################
681 | # BAIL #
682 | #######################
683 | return None
684 |
685 | if subreddit == "boottoobig":
686 | boot = True
687 | else:
688 | boot = False
689 |
690 | # I absolutely hate this method, would much rather just test length but people on StackOverflow bitch so
691 | if not customargs:
692 | image.add_title(title, boot)
693 | else:
694 | image.add_title(title=title, boot=boot, customargs=customargs, author=submission_author)
695 |
696 | imgur_url = self.upload(image)
697 |
698 | return imgur_url
699 |
700 | def process_gif(self, submission):
701 | """Process a gif.
702 |
703 | Notes:
704 | This is ineffecient and awful. I need to either research and get familiar with animated picture processing
705 | in python or call up on the expertise of someone experienced in the area; Also considering building a
706 | library to do so in a better/more suitable language and calling it as a subprocess, which would work great
707 | as well
708 |
709 | See my GfyPy project for information on how that library works.
710 |
711 | Args:
712 | submission (praw.models.Submission): Submission to process
713 |
714 | Returns:
715 | Gfycat URL
716 | """
717 |
718 | # TODO: hotfix framerate issues
719 |
720 | # sub = submission.subreddit.display_name
721 | url = submission.url
722 | title = submission.title
723 | # author = submission.author.name
724 |
725 | # If its a gifv and hosted on imgur, we're ok, anywhere else I cant verify it works
726 | if 'imgur' in url and url.endswith("gifv"):
727 | # imgur will give us a (however large) gif if we ask for it
728 | # thanks imgur <3
729 | url = url.rstrip('v')
730 | # Reddit Hosted gifs are going to be absolute hell, served via DASH which
731 | # Can be checked through a fallback url :)
732 | try:
733 | response = requests.get(url)
734 | # The nature of this throws tons of exceptions based on what users throw at the bot
735 | except Exception as error:
736 | logging.error(error)
737 | logging.error('Exception on image conversion lines.')
738 | return None
739 |
740 | img = Image.open(BytesIO(response.content))
741 | frames = []
742 |
743 | # Process Gif
744 | # We do this by creating a reddit image for every frame of the gif
745 | # This is godawful, but the impact on performance isn't too bad
746 |
747 | # Loop over each frame in the animated image
748 | for frame in ImageSequence.Iterator(img):
749 | # Draw the text on the frame
750 |
751 | # We'll create a custom RedditImage for each frame to avoid
752 | # redundant code
753 |
754 | r_frame = RedditImage(frame)
755 | r_frame.add_title(title, False)
756 |
757 | frame = r_frame.image
758 | # However, 'frame' is still the animated image with many frames
759 | # It has simply been seeked to a later frame
760 | # For our list of frames, we only want the current frame
761 |
762 | # Saving the image without 'save_all' will turn it into a single frame image, and we can then re-open it
763 | # To be efficient, we will save it to a stream, rather than to file
764 | b = BytesIO()
765 | frame.save(b, format="GIF")
766 | frame = Image.open(b)
767 |
768 | # The first successful image generation was 150MB, so lets see what all
769 | # Can be done to not have that happen
770 |
771 | # Then append the single frame image to a list of frames
772 | frames.append(frame)
773 | # Save the frames as a new image
774 | path_gif = 'temp.gif'
775 | # path_mp4 = 'temp.mp4'
776 | frames[0].save(path_gif, save_all=True, append_images=frames[1:])
777 | # ff = ffmpy.FFmpeg(inputs={path_gif: None},outputs={path_mp4: None})
778 | # ff.run()
779 |
780 | # noinspection PyBroadException
781 | try:
782 | ########################
783 | # UPLOAD TO GFYCAT #
784 | ########################
785 | url = self.upload_to_gfycat(path_gif).url
786 | remove(path_gif)
787 | except Exception as ex:
788 | logging.error('Gif Upload Failed with %s, Returning' % ex)
789 | remove(path_gif)
790 | return None
791 | # remove(path_mp4)
792 | return url
793 |
794 | @staticmethod
795 | def get_params_from_twitter(link):
796 | """ Get the paramaters that we shove into process_image_submission from a twitter link.
797 |
798 | Unfinished
799 |
800 | TODO: REWORK HOW PROCESS_IMAGE_SUBMISSION WORKS TO ALLOW IT TO NOT NEED TO USE PRAW MODELS
801 |
802 | Args:
803 | link:
804 |
805 | Returns:
806 |
807 | """
808 | page = requests.get(link)
809 | soup = BeautifulSoup(page.text, 'html.parser')
810 | tweet_text = soup.select(".tweet-text")
811 | tweet_text_raw = tweet_text[0] if len(tweet_text) > 0 else ""
812 | cleaned = BeautifulSoup(str(tweet_text_raw))
813 | invalid_tags = ['a', 'p']
814 | for tag in invalid_tags:
815 | for match in cleaned.findAll(tag):
816 | match.unwrap()
817 | twitpiclink = 'pic.twitter.com'
818 | nonfluff = str(cleaned).split(twitpiclink, 1)[0]
819 | return [nonfluff]
820 |
821 | def upload(self, reddit_image):
822 | """
823 | Upload self._image to imgur
824 |
825 | :type reddit_image: RedditImage
826 | :param reddit_image:
827 | :returns: imgur url if upload successful, else None
828 | :rtype: str, NoneType
829 | """
830 | path_png = 'temp.png'
831 | path_jpg = 'temp.jpg'
832 | reddit_image.image.save(path_png)
833 | reddit_image.image.save(path_jpg)
834 | # noinspection PyBroadException
835 | response = None
836 | try:
837 | # Upload to imgur using pyimgur
838 | response = self.upload_to_imgur(path_png)
839 | except ImgurException as ex:
840 | logging.error('ImgurException: ' % ex)
841 | # Likely too large
842 | logging.warning('png upload failed with %s, trying jpg' % ex)
843 | try:
844 | # Upload to imgur using pyimgur
845 | response = self.upload_to_imgur(path_jpg)
846 | except ImgurException as ex:
847 | logging.error('ImgurException: %s' % ex)
848 | logging.error('jpg upload failed with %s, returning' % ex)
849 | response = None
850 | except HTTPError as ex:
851 | logging.error('HTTPError: %s' % ex)
852 | logging.error('jpg upload failed with %s, returning' % ex)
853 | response = None
854 | except HTTPError as ex:
855 | logging.error('HTTPError: %s' % ex)
856 | logging.error('png upload failed with %s, returning' % ex)
857 | finally:
858 | remove(path_png)
859 | remove(path_jpg)
860 | if response is None:
861 | return None
862 | return response.link
863 |
864 | def upload_to_imgur(self, local_image_url):
865 | """Upload an image to imgur from a local image url
866 |
867 | Make sure to use my fork of PyImgur or errors will not be raised when image upload fails. The public PyImgur
868 | instead prints to console when it fails.
869 |
870 | Args:
871 | local_image_url (str): Path to local file
872 |
873 | Returns:
874 | Response from imgur.
875 |
876 | Raises:
877 | ImgurException: when image upload fails for whatever reason.
878 | """
879 | # Actually call pyimgur and upload image with it
880 | self.screen.set_current_action_status("Uploading to Imgur...", "")
881 | self.screen.set_imgur_status("Uploading...")
882 | response = self.imgur.upload_image(local_image_url, title="Uploaded by /u/%s" % self.config.bot_username)
883 | self.screen.set_current_action_status("Complete", "")
884 | self.screen.set_imgur_status("Connected")
885 | return response
886 |
887 | def upload_to_gfycat(self, local_gif_url):
888 | """Upload a local gif by path to gfycat
889 |
890 | Args:
891 | local_gif_url: path to gif
892 |
893 | Returns:
894 | GfyCat object
895 | """
896 | generated_gfycat = self.gfycat.upload_file(local_gif_url)
897 | return generated_gfycat
898 |
899 | def reply_imgur_url(self, url, submission, source_comment, custom_title=None, upscaled=False, customargs=None):
900 | """Reply to a comment with the imgur url generated
901 |
902 | Args:
903 | url (str): URL that was generated
904 | submission (praw.models.Submission): Submission that was processed
905 | source_comment (praw.models.Comment): Comment that requested processing
906 | custom_title (str): Custom title if it was added
907 | upscaled (bool): Whether image was upscaled for processing
908 | customargs (list[str]): List of custom arguments if any were passed
909 |
910 | Returns:
911 | True if reply succeeded, false otherwise
912 | """
913 |
914 | self.screen.set_current_action_status('Creating reply', "")
915 | if submission.subreddit.display_name.lower() in self.config.get_minimal_sub_list():
916 | reply = messages.minimal_reply_template(
917 | image_url=url,
918 | nsfw="(NSFW)"
919 | )
920 | elif submission.subreddit.display_name.lower() == "dankmemesfromsite19":
921 | # noinspection PyTypeChecker
922 | reply = messages.site19_template.format(
923 | image_url=url,
924 | warntag="Custom titles/arguments are in beta" if customargs else "",
925 | custom="custom " if custom_title and len(custom_title) > 0 else "",
926 | nsfw="(NSFW)" if submission.over_18 else '',
927 | upscaled=' (image was upscaled)\n\n' if upscaled else '',
928 | submission_id=submission.id
929 | )
930 | elif submission.subreddit.display_name.lower() == "de":
931 | # noinspection PyTypeChecker
932 | reply = messages.de_reply_template.format(
933 | image_url=url,
934 | warntag="" if customargs else "",
935 | custom="anpassen " if custom_title and len(custom_title) > 0 else "",
936 | nsfw="(NSFW)" if submission.over_18 else '',
937 | upscaled=' (Das Bild wurde in der Größe geändert)\n\n' if upscaled else ''
938 | )
939 | else:
940 | # noinspection PyTypeChecker
941 | reply = messages.standard_reply_template.format(
942 | image_url=url,
943 | warntag="Custom titles/arguments are in beta" if customargs else "",
944 | custom="custom " if custom_title and len(custom_title) > 0 else "",
945 | nsfw="(NSFW)" if submission.over_18 else '',
946 | upscaled=' (image was upscaled)\n\n' if upscaled else '',
947 | submission_id=submission.id
948 | )
949 | if submission.subreddit.display_name in self.config.get_ban_sub_list():
950 | reply = messages.banned_PM_template.format(
951 | image_url=url,
952 | warntag="Custom titles/arguments are in beta" if customargs else "",
953 | custom="custom " if custom_title and len(custom_title) > 0 else "",
954 | nsfw="(NSFW)" if submission.over_18 else '',
955 | upscaled=' (image was upscaled)\n\n' if upscaled else '',
956 | submission_id=submission.id
957 | )
958 | # If we're banned shoot this to the sub. rest of the stuff can run, it has no effect
959 | source_comment.author.message("Your Title2ImageBot'd Image", reply)
960 | try:
961 | if source_comment:
962 | #######################
963 | # REPLY #
964 | #######################
965 | source_comment.reply(reply)
966 | else:
967 | #######################
968 | # REPLY TO SUB #
969 | #######################
970 | submission.reply(reply)
971 | except praw.exceptions.APIException as error:
972 | logging.error('Reddit api error, we\'ll try to repost later | %s', error)
973 | return False
974 | except Exception as error:
975 | logging.error('Cannot reply, skipping submission | %s', error)
976 | return False
977 |
978 | if customargs:
979 | pls = ''.join(customargs)
980 | else:
981 | pls = ""
982 |
983 | if custom_title:
984 | sid = submission.id + custom_title + pls
985 | else:
986 | sid = submission.id + pls
987 |
988 | self.database.submission_insert(sid, submission.author.name, submission.title, url)
989 | return True
990 |
991 |
992 | class RedditImage:
993 | """Reddit Image class
994 |
995 | A majority of this class is the work of gerenook, the author of the original bot. Its ingenious work, and
996 | the bot absolutely could not function without it. Anything dumb here is my (CalicoCatalyst) work.
997 | custom arguments are my work.
998 | I'm going to do my best to document it.
999 |
1000 | Attributes:
1001 | image (Image): PIL.Image object. Once methods are ran, will contain the output as well.
1002 | upscaled (bool): Was the image upscaled?
1003 | title (str): Title we add to the image
1004 | """
1005 |
1006 | margin = 10
1007 | min_size = 500
1008 | # font_file = 'seguiemj.ttf'
1009 | font_file = 'roboto-emoji.ttf'
1010 | font_scale_factor = 16
1011 | # Regex to remove resolution tag styled as such: '[1000 x 1000]'
1012 | regex_resolution = re.compile(r'\s?\[[0-9]+\s?[xX*×]\s?[0-9]+\]')
1013 |
1014 | def __init__(self, image):
1015 | """Create an image object, and pass an image file to it. The RedditImage object then allows us to modify it.
1016 |
1017 | This should be an `extends Image` reeeeee
1018 |
1019 | Args:
1020 | image (Optional[any]): Image to be processed
1021 | """
1022 |
1023 | self.image = image
1024 | self.upscaled = False
1025 | self.title = ""
1026 |
1027 | width, height = image.size
1028 | # upscale small images
1029 | if image.size < (self.min_size, self.min_size):
1030 | if width < height:
1031 | factor = self.min_size / width
1032 | else:
1033 | factor = self.min_size / height
1034 | self.image = self.image.resize((ceil(width * factor),
1035 | ceil(height * factor)),
1036 | Image.LANCZOS)
1037 | self.upscaled = True
1038 | self._width, self._height = self.image.size
1039 | self._font_title = ImageFont.truetype(
1040 | self.font_file,
1041 | self._width // self.font_scale_factor
1042 | )
1043 |
1044 | def __str__(self):
1045 | """ Return the title of the image
1046 |
1047 | Returns:
1048 | Title of the image.
1049 | """
1050 | return self.title
1051 |
1052 | def _split_title(self, title):
1053 | """ Split the title into different lines for certain subreddits
1054 |
1055 | Args:
1056 | title (str): Title to split
1057 |
1058 | Returns:
1059 | Title that's been split
1060 |
1061 | """
1062 | lines = ['']
1063 | all_delimiters = [',', ';', '.']
1064 | delimiter = None
1065 | for character in title:
1066 | # don't draw ' ' on a new line
1067 | if character == ' ' and not lines[-1]:
1068 | continue
1069 | # add character to current line
1070 | lines[-1] += character
1071 | # find delimiter
1072 | if not delimiter:
1073 | if character in all_delimiters:
1074 | delimiter = character
1075 | # end of line
1076 | if character == delimiter:
1077 | lines.append('')
1078 | # if a line is too long, wrap title instead
1079 | for line in lines:
1080 | if self._font_title.getsize(line)[0] + RedditImage.margin > self._width:
1081 | return self._wrap_title(title)
1082 | # remove empty lines (if delimiter is last character)
1083 | return [line for line in lines if line]
1084 |
1085 | def _wrap_title(self, title):
1086 | """Actually wrap the title.
1087 |
1088 | Args:
1089 | title: Title to wrap
1090 |
1091 | Returns:
1092 | Wrapped title
1093 | """
1094 | lines = ['']
1095 | line_words = []
1096 | words = title.split()
1097 | for word in words:
1098 | line_words.append(word)
1099 | lines[-1] = ' '.join(line_words)
1100 | if self._font_title.getsize(lines[-1])[0] + RedditImage.margin > self._width:
1101 | lines[-1] = lines[-1][:-len(word)].strip()
1102 | lines.append(word)
1103 | line_words = [word]
1104 | # remove empty lines
1105 | return [line for line in lines if line]
1106 |
1107 | def add_title(self, title, boot, bg_color='#fff', text_color='#000', customargs=None, author=None):
1108 | """Add the title to an image
1109 |
1110 | return function is not used.
1111 |
1112 | Args:
1113 | title (str): The title to add
1114 | boot (bool): Is this a bootTooBig post
1115 | bg_color (str): Background of the title section
1116 | text_color (str): Foreground (text) color
1117 | customargs (str): Custom arguments passed to the image parser
1118 | author (str): Author of the submission
1119 |
1120 | Returns:
1121 | Edited image
1122 | """
1123 | self.title = title
1124 |
1125 | title_centering = False
1126 | dark_mode = False
1127 | tag_author = False
1128 |
1129 | if customargs is not None:
1130 | for arg in customargs:
1131 | if arg == "center":
1132 | title_centering = True
1133 | if arg == "dark":
1134 | dark_mode = True
1135 | if arg == "tagauth":
1136 | tag_author = True
1137 |
1138 | if dark_mode:
1139 | bg_color = '#000'
1140 | text_color = '#fff'
1141 |
1142 | # remove resolution appended to title (e.g. ' [1000 x 1000]')
1143 | title = RedditImage.regex_resolution.sub('', title)
1144 | line_height = self._font_title.getsize(title)[1] + RedditImage.margin
1145 | lines = self._split_title(title) if boot else self._wrap_title(title)
1146 | whitespace_height = (line_height * len(lines)) + RedditImage.margin
1147 | tagauthheight = 0
1148 | if tag_author:
1149 | tagauthheight = 50
1150 | left_margin = 10
1151 | new = Image.new('RGB', (self._width, self._height + whitespace_height + tagauthheight), bg_color)
1152 | new.paste(self.image, (0, whitespace_height))
1153 | draw = ImageDraw.Draw(new)
1154 | for i, line in enumerate(lines):
1155 | w, h = self._font_title.getsize(line)
1156 | left_margin = ((self._width - w) / 2) if title_centering else RedditImage.margin
1157 | draw.text((left_margin, i * line_height + RedditImage.margin),
1158 | line, text_color, self._font_title)
1159 |
1160 | if tag_author:
1161 | draw.text((left_margin, self._height + whitespace_height + self.margin),
1162 | author, text_color, self._font_title)
1163 | self._width, self._height = new.size
1164 | self.image = new
1165 | return self.image
1166 |
1167 | class Configuration(object):
1168 | """
1169 | Attributes:
1170 | configfile (ConfigParser): Configuration Parser
1171 | maintainer (str): Reddit Username of the bot maintainer.
1172 | bot_username (str): Reddit Username of the bot
1173 | """
1174 |
1175 | def __init__(self, config_file):
1176 | """Interface for interacting with the bot's config file.
1177 |
1178 | Args:
1179 | config_file (str): Name of the config file.
1180 | """
1181 | self._config = configparser.ConfigParser()
1182 | self._config.read(config_file)
1183 | self.configfile = self._config
1184 | self.maintainer = self._config['Title2ImageBot']['maintainer']
1185 | self.bot_username = self._config['RedditAuth']['username']
1186 |
1187 | def get_automatic_processing_subs(self):
1188 | """Return the subreddits that should be automatically processed
1189 |
1190 | In the bot configuration file, there are configuration sections. Any other sections are subs to automatically
1191 | parse. So here, we remove the config sections and return an array of the rest
1192 |
1193 | Returns:
1194 | List of subs to automatically parse.
1195 |
1196 | """
1197 | sections = self._config.sections()
1198 | sections.remove("RedditAuth")
1199 | sections.remove("GfyCatAuth")
1200 | sections.remove("IgnoreList")
1201 | sections.remove("MinimalList")
1202 | sections.remove("ImgurAuth")
1203 | sections.remove("Title2ImageBot")
1204 | sections.remove("BanList")
1205 | return sections
1206 |
1207 | def get_user_ignore_list(self):
1208 | """Get list of users to ignore
1209 |
1210 | Typically this is bots that pester this bot (accidentally).
1211 |
1212 | Returns:
1213 | List of usernames to ignore
1214 | """
1215 | ignore_list = []
1216 | for i in self._config.items("IgnoreList"):
1217 | ignore_list.append(i[0])
1218 | return ignore_list
1219 |
1220 | def get_minimal_sub_list(self):
1221 | """Get list of subs to use the minimal response format on
1222 |
1223 | Tons of issues with long responses, links to usernames, formatted links, on some subs.
1224 |
1225 | Returns:
1226 | List of minimal-format subs
1227 |
1228 | """
1229 | minimal_list = []
1230 | for i in self._config.items("MinimalList"):
1231 | minimal_list.append(i[0])
1232 | return minimal_list
1233 |
1234 | def get_ban_sub_list(self):
1235 | """Get list of subs to ignore completely
1236 |
1237 | Returns:
1238 | List of subs to ignore
1239 |
1240 | """
1241 | ban_list = []
1242 | for i in self._config.items("BanList"):
1243 | ban_list.append(i[0])
1244 | return ban_list
1245 |
1246 | def get_gfycat_client_config(self):
1247 | """Generate the gfycat client with the info in the config
1248 |
1249 | Returns:
1250 | Gfycat client
1251 |
1252 | """
1253 | client_id = self._config['GfyCatAuth']['publicKey']
1254 | client_secret = self._config['GfyCatAuth']['privateKey']
1255 | username = self._config['GfyCatAuth']['username']
1256 | password = self._config['GfyCatAuth']['password']
1257 | client = gfycat.GfyCatClient(client_id, client_secret, username, password)
1258 | return client
1259 |
1260 | def auth_reddit_from_config(self):
1261 | """Generate the reddit client with the info in the config
1262 |
1263 | Returns:
1264 | praw Reddit object
1265 |
1266 | """
1267 | return (praw.Reddit(client_id=self._config['RedditAuth']['publicKey'],
1268 | client_secret=self._config['RedditAuth']['privateKey'],
1269 | username=self._config['RedditAuth']['username'],
1270 | password=self._config['RedditAuth']['password'],
1271 | user_agent=self._config['RedditAuth']['userAgent']))
1272 |
1273 | def get_imgur_client_config(self):
1274 | """Generate the imgur client from the info in the config
1275 |
1276 | Returns:
1277 | PyImgur client.
1278 |
1279 | """
1280 | return pyimgur.Imgur(self._config['ImgurAuth']['publicKey'])
1281 |
1282 |
1283 | class BotDatabase(object):
1284 | """Interface for the SQLite 3 Database that stores processed comments/submissions
1285 | """
1286 | def __init__(self, db_filename, interface):
1287 | """Interface for the SQLite 3 Database that stores processed comments/submissions
1288 |
1289 | Args:
1290 | db_filename (str): Filename of the SQLite 3 database
1291 | interface (CLI): CLI for the bot to allow live updates.
1292 | """
1293 | self._interface = interface
1294 | self._sql_conn = sqlite3.connect(db_filename, check_same_thread=False)
1295 | self._sql = self._sql_conn.cursor()
1296 |
1297 | def cleanup(self):
1298 | """Clean up SQL connections before quitting the program.
1299 | """
1300 | self._sql_conn.commit()
1301 | self._sql_conn.close()
1302 |
1303 | def message_exists(self, message_id):
1304 | """Check if message exists in messages table
1305 |
1306 | Args:
1307 | message_id (str): ID of the message to check.
1308 |
1309 | Returns:
1310 | True if it exists in the database, False otherwise.
1311 | """
1312 | self._interface.set_data_status("Querying...")
1313 | self._sql.execute('SELECT EXISTS(SELECT 1 FROM messages WHERE id=?)', (message_id,))
1314 | self._interface.set_data_status("Connected")
1315 | if self._sql.fetchone()[0]:
1316 | return True
1317 | else:
1318 | return False
1319 |
1320 | def submission_exists(self, submission_id):
1321 | """Check if submission exists in submissions table
1322 |
1323 | Args:
1324 | submission_id (str): ID of submission to check
1325 |
1326 | Returns:
1327 | True if submission exists in database, False otherwise
1328 | """
1329 | self._interface.set_data_status("Querying...")
1330 | self._sql.execute('SELECT EXISTS(SELECT 1 FROM submissions WHERE id=?)', (submission_id,))
1331 | self._interface.set_data_status("Connected")
1332 | if self._sql.fetchone()[0]:
1333 | return True
1334 | else:
1335 | return False
1336 |
1337 | def message_parsed(self, message_id):
1338 | """Check if message was parsed.
1339 |
1340 | Notes:
1341 | This is unused, as there is currently not a way to set the parsed flag, which this checks.
1342 | *However,* a message will not be added until it is parsed. In this version of the bot, it can
1343 | be checked using the `message_exists()` function.
1344 |
1345 | This is simply a left-over function
1346 | from the previous developer's work that I included for the sake of including it. I'll leave it in
1347 | just in case it is ever needed in the future
1348 |
1349 | Warnings:
1350 | Check if the passed message ID exists before calling this, as this method does not do so.
1351 |
1352 | Args:
1353 | message_id (str): ID of message to check
1354 |
1355 | Returns:
1356 | True if the parsed flag is set for a message, False otherwise
1357 | """
1358 | self._interface.set_data_status("Querying...")
1359 | self._sql.execute('SELECT EXISTS(SELECT 1 FROM messages WHERE id=? AND parsed=1)', (message_id,))
1360 | self._interface.set_data_status("Connected")
1361 | if self._sql.fetchone()[0]:
1362 | return True
1363 | else:
1364 | return False
1365 |
1366 | def message_insert(self, message_id, message_author, subject, body):
1367 | """Insert message once it has been parsed, along with some other info about it, into messages table
1368 |
1369 | Args:
1370 | message_id (str): Message ID (main key)
1371 | message_author (str): Author of message
1372 | subject (str): Subject of the message, will be the type of message recieved
1373 | body (str): Contents of the comment/message/whatever
1374 |
1375 | """
1376 | self._interface.set_data_status("Querying...")
1377 | self._sql.execute('INSERT INTO messages (id, author, subject, body) VALUES (?, ?, ?, ?)',
1378 | (message_id, message_author, subject, body))
1379 | self._sql_conn.commit()
1380 | self._interface.set_data_status("Connected")
1381 |
1382 | def submission_select(self, submission_id):
1383 | """Return a database of information about a submission from the submissions table.
1384 |
1385 | Args:
1386 | submission_id (str): ID of the submission
1387 |
1388 | Returns:
1389 | {
1390 | 'id': ID of the submission,
1391 | 'author': Author of the submission,
1392 | 'title': Title of the submission,
1393 | 'url': Reddit URL of the submission,
1394 | 'imgur_url': Imgur URL that the parsed submission was uploaded to,
1395 | 'retry': Retry flag (leftover, useless)
1396 | 'timestamp': Timestamp of the post.}
1397 | """
1398 | self._interface.set_data_status("Querying...")
1399 | self._sql.execute('SELECT * FROM submissions WHERE id=?', (submission_id,))
1400 | result = self._sql.fetchone()
1401 | self._interface.set_data_status("Connected")
1402 | if not result:
1403 | return None
1404 | return {
1405 | 'id': result[0],
1406 | 'author': result[1],
1407 | 'title': result[2],
1408 | 'url': result[3],
1409 | 'imgur_url': result[4],
1410 | 'retry': result[5],
1411 | 'timestamp': result[6]
1412 | }
1413 |
1414 | def submission_insert(self, submission_id, submission_author, title, url):
1415 | """Insert a submission into the submission database
1416 |
1417 | Args:
1418 | submission_id (str): ID of the submission (main key)
1419 | submission_author (str): Author of the submission
1420 | title (str): Title of the submission
1421 | url (str): Reddit URL of the submission
1422 |
1423 | """
1424 | self._interface.set_data_status("Querying...")
1425 | """Insert submission into submissions table"""
1426 | self._sql.execute('INSERT INTO submissions (id, author, title, url) VALUES (?, ?, ?, ?)',
1427 | (submission_id, submission_author, title, url))
1428 | self._sql_conn.commit()
1429 | self._interface.set_data_status("Connected")
1430 |
1431 | def submission_set_retry(self, submission_id, delete_message=False, message=None):
1432 | """Set retry flag on database
1433 |
1434 | Args:
1435 | submission_id:
1436 | delete_message:
1437 | message:
1438 |
1439 | Notes:
1440 | Unused. Set retry flag when a submission isn't able to be uploaded.
1441 |
1442 | TODO:
1443 | Actually use this
1444 |
1445 | Raises:
1446 | TypeError: If delete_message is true, the message needs to be passed.
1447 | """
1448 | self._interface.set_data_status("Querying...")
1449 | self._sql.execute('UPDATE submissions SET retry=1 WHERE id=?', (submission_id,))
1450 | if delete_message:
1451 | if not message:
1452 | raise TypeError('If delete_message is True, message must be set')
1453 | self._sql.execute('DELETE FROM messages WHERE id=?', (message.id,))
1454 | self._sql_conn.commit()
1455 | self._interface.set_data_status("Connected")
1456 |
1457 | def submission_clear_retry(self, submission_id):
1458 | """Clear retry flag on database.
1459 |
1460 | Args:
1461 | submission_id (str): ID of the submission
1462 |
1463 | """
1464 | self._interface.set_data_status("Querying...")
1465 | self._sql.execute('UPDATE submissions SET retry=0 WHERE id=?', (submission_id,))
1466 | self._sql_conn.commit()
1467 | self._interface.set_data_status("Connected")
1468 |
1469 | def submission_set_imgur_url(self, submission_id, imgur_url):
1470 | """Set imgur url for a submission
1471 |
1472 | Notes:
1473 | Not currently used.
1474 |
1475 | Args:
1476 | submission_id (str): Submission ID
1477 | imgur_url (str): IMGUR url to set
1478 | """
1479 | self._interface.set_data_status("Querying...")
1480 | self._sql.execute('UPDATE submissions SET imgur_url=? WHERE id=?',
1481 | (imgur_url, submission_id))
1482 | self._sql_conn.commit()
1483 | self._interface.set_data_status("Connected")
1484 |
1485 |
1486 | class CLI(object):
1487 | """NCurses interface for ease of management
1488 |
1489 | Attributes:
1490 | reddituser (str): Name of the user the bot logs in with
1491 | redditstatus (str): Status of the connection to reddit servers
1492 | imgurstatus (str): Status of the connection to imgur servers
1493 | datastatus (str): Status of the connection to the SQLite database
1494 | streamstatus (str): Status of the comment psuedostream.
1495 | loglvl (str): Logging level for the bot logs
1496 | killflag (bool): If this is true, stuff will stop updating.
1497 | """
1498 |
1499 | def __init__(self):
1500 | """ncurses interface for ease of management.
1501 | """
1502 |
1503 | self.stdscr = curses.initscr()
1504 | curses.noecho()
1505 | curses.cbreak()
1506 | curses.curs_set(0)
1507 |
1508 | _, self.cols = self.stdscr.getmaxyx()
1509 |
1510 | self.reddituser = "Updating..."
1511 | self.redditstatus = "Not Connected"
1512 | self.imgurstatus = "Not Connected"
1513 | self.datastatus = "Not Connected"
1514 | self.streamstatus = "Not Connected"
1515 | self.loglvl = "Debug" if (logging.getLogger().level == logging.DEBUG) else "Standard"
1516 | self._catx = self.cols - 36
1517 | self._caty = 1
1518 | self.killflag = False
1519 |
1520 | def set_reddit_user(self, reddituser):
1521 | """Set the reddit user and update CLI
1522 |
1523 | Args:
1524 | reddituser (str): username
1525 |
1526 | """
1527 | self.reddituser = reddituser
1528 | self.update_bot_status_info()
1529 |
1530 | def set_reddit_status(self, redditstatus):
1531 | """Set the status of the reddit connection and update CLI
1532 |
1533 | Args:
1534 | redditstatus (str): status of reddit connection
1535 | """
1536 | self.redditstatus = redditstatus
1537 | self.update_bot_status_info()
1538 |
1539 | def set_imgur_status(self, imgurstatus):
1540 | """Set the status of the imgur connection and update CLI
1541 |
1542 | Args:
1543 | imgurstatus (str): status of imgur connection
1544 | """
1545 | self.imgurstatus = imgurstatus
1546 | self.update_bot_status_info()
1547 |
1548 | def set_data_status(self, datastatus):
1549 | """Set the status of the database connection and update CLI
1550 |
1551 | Args:
1552 | datastatus (str): status of database connection
1553 | """
1554 | self.datastatus = datastatus
1555 | self.update_bot_status_info()
1556 |
1557 | def set_stream_status(self, streamstatus):
1558 | """Set the status of the comment stream and update CLI
1559 |
1560 | Warnings:
1561 | Do not access this method from inside a different thread. The CLI will slowly glitch out and be unusable.
1562 |
1563 | Args:
1564 | streamstatus (str): status of stream connection
1565 | """
1566 | self.streamstatus = streamstatus
1567 | self.update_bot_status_info()
1568 |
1569 | def update_bot_status_info(self):
1570 | """Update the entire CLI view.
1571 | """
1572 | if self.killflag:
1573 | # stop updating curses. I dont know whats calling this thread after things are done
1574 | # TODO: thread for normal loop as well
1575 | return
1576 |
1577 | self.stdscr.refresh()
1578 | self.clear_line(0)
1579 | self.clear_line(5)
1580 | self.clear_line(6)
1581 | self.clear_line(7)
1582 | self.clear_line(8)
1583 | self.clear_line(9)
1584 | self.stdscr.addstr(0, 0, "Title2ImageBot Version %s by CalicoCatalyst" % __version__)
1585 | self.stdscr.addstr(5, 0, "Reddit Username : %s" % self.reddituser)
1586 | self.stdscr.addstr(6, 0, "Reddit Status : %s" % self.redditstatus)
1587 | self.stdscr.addstr(7, 0, "Imgur Status : %s" % self.imgurstatus)
1588 | self.stdscr.addstr(8, 0, "Comment Stream : %s" % self.streamstatus)
1589 | self.stdscr.addstr(9, 0, "Database Status : %s" % self.datastatus)
1590 | self.stdscr.addstr(10, 0, "Logging Mode : %s" % self.loglvl)
1591 | self.print_cat(self._catx, self._caty)
1592 | self.stdscr.refresh()
1593 |
1594 | @staticmethod
1595 | def get_progress_line(iteration, total, prefix='', suffix='', decimals=1, bar_length=25):
1596 | """Generate a progress line. Credit to some guy on stackoverflow, lost the link
1597 |
1598 | Args:
1599 | iteration (int): Iteration out of total
1600 | total (int): Total amount of iterations
1601 | prefix (str): Prefix of the string
1602 | suffix (str): Suffix of the string
1603 | decimals (int): How many decimals to count
1604 | bar_length (int): Length of characters for bar
1605 |
1606 | Returns:
1607 | The progress bar created.
1608 | """
1609 |
1610 | str_format = "{0:." + str(decimals) + "f}"
1611 | percents = str_format.format(100 * (iteration / float(total)))
1612 | filled_length = int(round(bar_length * iteration / float(total)))
1613 | bar = '+' * filled_length + '-' * (bar_length - filled_length)
1614 |
1615 | return '%s|%s| %s%s %s' % (prefix, bar, percents, '%', suffix)
1616 |
1617 | def set_current_action_status(self, action, statusline):
1618 | """Set the current action and status of said action
1619 |
1620 | Args:
1621 | action (str): Action currently being ran by the program
1622 | statusline (str): Progress bar (generated with `get_progress_line`
1623 | """
1624 | self.clear_line(14)
1625 | self.clear_line(15)
1626 | self.stdscr.addstr(14, 0, "%s" % action)
1627 | self.stdscr.addstr(15, 0, "%s" % statusline)
1628 | self.print_cat(self._catx, self._caty)
1629 | self.stdscr.refresh()
1630 |
1631 | def clear_line(self, y):
1632 | """Clear a line in the CLI, then reprint the entire cat.
1633 |
1634 | Args:
1635 | y (int): line to clear
1636 |
1637 | """
1638 | self.stdscr.move(y, 0)
1639 | self.stdscr.clrtoeol()
1640 | self.print_cat(self._catx, self._caty)
1641 | self.stdscr.refresh()
1642 |
1643 | def print_cat(self, startx, starty):
1644 | """Print a cat in curses and a little credit line.
1645 |
1646 | Warnings:
1647 | Warning: this cat is very cute
1648 |
1649 | Args:
1650 | startx (int): Distance from the left where the cat should start
1651 | starty (int): Distance from the top where the cat should start
1652 | """
1653 | if self.cols < 66:
1654 | return
1655 | line1 = ' (`.-,\')'
1656 | line2 = ' .-\' ;'
1657 | line3 = ' _.-\' , `,-'
1658 | line4 = ' _ _.-\' .\' /._'
1659 | line5 = ' .\' ` _.-. / ,\'._;)'
1660 | line6 = ' ( . )-| ('
1661 | line7 = ' )`,_ ,\'_,\' \_;)'
1662 | line8 = '(\'_ _,\'.\' (___,))'
1663 | line9 = ' `-:;.-\''
1664 | lines = [line1, line2, line3, line4, line5, line6, line7, line8, line9]
1665 |
1666 | num = 0
1667 |
1668 | for i in range(starty, starty + 9):
1669 | self.stdscr.addstr(i, startx, lines[num])
1670 | num += 1
1671 | self.stdscr.addstr(starty+11, startx-15, 'Reddit Bot Interactive CLI by CalicoCatalyst')
1672 |
1673 |
1674 | def main():
1675 | parser = argparse.ArgumentParser(description='Bot To Add Titles To Images')
1676 | parser.add_argument('-d', '--debug', help='Enable Debug Logging', action='store_true')
1677 | parser.add_argument('-l', '--loop', help='Enable Looping Function', action='store_true')
1678 | parser.add_argument('limit', help='amount of submissions/messages to process each cycle',
1679 | type=int)
1680 | parser.add_argument('interval', help='time (in seconds) to wait between cycles', type=int)
1681 |
1682 | args = parser.parse_args()
1683 |
1684 | current_timestamp = str(time.time())
1685 | os.rename("logs/latest.log", "logs/logfile-" + current_timestamp + ".log")
1686 |
1687 | # Turn on debug mode with -d flag
1688 | if args.debug:
1689 | logging.basicConfig(filename="logs/latest.log", format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S',
1690 | level=logging.DEBUG)
1691 | else:
1692 | logging.basicConfig(filename="logs/latest.log", format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S',
1693 | level=logging.INFO)
1694 |
1695 | # Status line
1696 | # TODO: Add this info to curses
1697 | # logging.info('Bot initialized, processing the last %s submissions/messages every %s seconds' % (args.limit,
1698 | # args.interval))
1699 |
1700 | # Set up CLI with curses
1701 |
1702 | interface = CLI()
1703 |
1704 | interface.update_bot_status_info()
1705 |
1706 | # Set up database
1707 | configuration = Configuration("config.ini")
1708 | database = BotDatabase("t2ib.sqlite", interface)
1709 |
1710 | # Begin making our CLI screen
1711 |
1712 | bot = TitleToImageBot(configuration, database, interface)
1713 |
1714 | # Status testing and stuff for the CLI
1715 |
1716 | # Get username. This will also let us know if we cant connect at all. thanks praw author guy.
1717 | # noinspection PyBroadException
1718 | try:
1719 | interface.set_reddit_user(bot.reddit.user.me().name)
1720 | interface.set_reddit_status("Connected")
1721 | except Exception as ex:
1722 | interface.set_reddit_user("Could not connect")
1723 | interface.set_reddit_status("Unable to authenticate with %s" % ex)
1724 |
1725 | # This will test our connection to imgur servers by attempting to get an image I know exists. \
1726 | # noinspection PyBroadException
1727 | try:
1728 | bot.imgur.get_image('S1jmapR')
1729 | interface.set_imgur_status("Connected")
1730 | except Exception as ex:
1731 | interface.set_imgur_status("Unable to connect with %s" % ex)
1732 |
1733 | # This will test our connection to the database by getting a post i put in it for this purpose
1734 | # When creating a new database, you should add a submission titled "aaaaaa" for this purpose.
1735 | # TODO: find a less clunky method of doing this.
1736 | # noinspection PyBroadException
1737 | try:
1738 | database.submission_exists("aaaaaa")
1739 | interface.set_data_status("Connected")
1740 | except Exception as ex:
1741 | interface.set_data_status("Unable to connect. Yikes. With %s" % ex)
1742 |
1743 | # logging.debug('Debug Enabled')
1744 |
1745 | # noinspection PyBroadException
1746 | try:
1747 | if not args.loop:
1748 | bot.call_checks(args.limit)
1749 | interface.set_current_action_status('Checking Complete, Exiting Program', "")
1750 | exit(0)
1751 |
1752 | bot.start_comment_streaming_thread()
1753 | while True:
1754 | bot.call_checks(args.limit)
1755 | interface.set_current_action_status('Checking Complete', "")
1756 | time.sleep(args.interval)
1757 | except KeyboardInterrupt:
1758 | # WARNING: THIS WILL NOT WORK WITH FOREVER.SH FOR WHATEVER DUMB REASON
1759 | print("Command line debugging enabled")
1760 | # TODO: Clean this up w/ curse
1761 |
1762 | bot.stop_comment_streaming_thread()
1763 | curses.echo()
1764 | curses.nocbreak()
1765 | curses.endwin()
1766 | interface.killflag = True
1767 | #os.system('stty sane')
1768 | #os.system('clear')
1769 | print('Interface and threads killed. Command line debugging enabled. Non-threaded functions still active\n')
1770 | print('Ctrl+C again to end the program.\n')
1771 | maxfails = 5
1772 | fails = 0
1773 | while True:
1774 | try:
1775 | command = input(">>> ")
1776 | except KeyboardInterrupt:
1777 | break
1778 | except EOFError:
1779 | if fails == 5:
1780 | break
1781 | fails += 1
1782 | continue
1783 | if str(command) == "quit":
1784 | break
1785 | try:
1786 | exec(command)
1787 | except Exception as ex:
1788 | print("Failed with exception %s\n" % ex)
1789 | logging.debug("KeyboardInterrupt Detected, Cleaning up and exiting...")
1790 | print("Cleaning up and exiting...")
1791 | database.cleanup()
1792 |
1793 | curses.echo()
1794 | curses.nocbreak()
1795 | curses.endwin()
1796 | os.system('stty sane')
1797 | os.system('clear')
1798 | os.system('clear')
1799 | exit(0)
1800 |
1801 | except Exception as ex:
1802 | bot.reddit.redditor(bot.config.maintainer).message("bot crash", "Bot Crashed :p %s" % ex)
1803 | curses.echo()
1804 | curses.nocbreak()
1805 | curses.endwin()
1806 |
1807 |
1808 | if __name__ == '__main__':
1809 | main()
1810 |
--------------------------------------------------------------------------------