├── .github └── CONTRIBUTING.md ├── .gitignore ├── LICENSE ├── README.html ├── README.md ├── changelog.txt ├── doc ├── manual.html └── manual.md ├── screenshots ├── ttbp-compose.png ├── ttbp-entries.png └── ttbp-main.png ├── setup.py └── ttbp ├── __init__.py ├── chatter.py ├── config ├── __init__.py ├── banner-beta.txt └── defaults │ ├── footer.txt │ ├── header.txt │ └── style.css ├── core.py ├── gopher.py ├── ttbp.py ├── update.py └── util.py /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING GUIDELINES FOR TTBP 2 | 3 | Hi! Thanks for your interest in working on this project with me! 4 | 5 | Please understand that this may not be a beginner-friendly project to contribute 6 | to; this is because I am not a very experienced developer, and I'm still 7 | learning good practices for facilitating collaborative dev work. I'm open to 8 | receiving feedback and discussion even if you're new to writing Python, 9 | programming, or collaborative work in general, but I'd appreciate some extra 10 | patience and open communication. 11 | 12 | In general, please try to give me a heads up if you're intending to work on this 13 | codebase before either starting on it or submitting a PR. I'm fairly 14 | self-conscious about my code, so I want the best opportunity for respectful 15 | engagement, which lets me learn from my mistakes while still providing good 16 | software for end users. 17 | 18 | If you do not have the skills to contribute to the coding end, I still welcome 19 | your feedback about your experience using ttbp; please feel free to contact me 20 | with anything from vague appreciation to gripes about the interface! I'm 21 | interested in understanding how you feel about this program, and any information 22 | you give me will help me build a smoother tool for everyone! The ["feedback 23 | wanted"](https://github.com/modgethanc/ttbp/issues?q=is%3Aissue+is%3Aopen+label%3A"feedback+wanted") 24 | tag is a great place to start. 25 | 26 | ## Contacting Me 27 | 28 | You can send me tildemail at ~endorphant on tilde.town, or catch me in IRC 29 | (please send me a PM there, since I don't often monitor the main channel). I 30 | might not respond for a few days; this is normal! If you're feeling ignored, 31 | please feel free to open an issue in this repo, since that will ping my personal 32 | email directly, and we can catch up then. 33 | 34 | ## Bugs 35 | 36 | If you find a bug (such as: ttbp crashed while you were running it, an 37 | unexpected behavior happened, some parts of the interface don't line up 38 | correctly, etc.), please feel free to file an issue with this repo. You can also 39 | send me tildemail. Be as descriptive as possible! Describe the last few things 40 | that happened, if you remember them, and attach some representation of the bug 41 | (copy and paste the terminal output, or take a screenshot). 42 | 43 | If you can fix a bug that you found, please document the bug first by opening an 44 | issue, and check in with me before you start working on it. If there's a serious 45 | use-breaking bug in the current live code, and I'm unreachable for more than a 46 | few days, go ahead and fix it and send a PR, and I'll get to it when I can. 47 | 48 | ## Features 49 | 50 | Features that I intend to implement are often documented under ["upcoming 51 | features"](https://github.com/modgethanc/ttbp/issues?q=is%3Aissue+is%3Aopen+label%3A"upcoming+features") 52 | in the issue tracker. Feel free to weigh in on any features listed there! 53 | 54 | If there's a feature you'd like to see, but it isn't listed as upcoming, please 55 | check the list of issues and see if it's been discussed before (including in 56 | [closed 57 | issues](https://github.com/modgethanc/ttbp/issues?q=is%3Aissue+is%3Aclosed)). 58 | You're welcome to open a new issue if it seems like there isn't any history 59 | about your idea, and you can also send me tildemail to ask me about it. 60 | 61 | Please do not start coding new features without discussing your ideas with me 62 | first. I have some fairly strong ideas about how ttbp should work, and I don't 63 | want anyone to waste their time building new features that don't align with my 64 | vision for this project. That said, I'm definitely interested in hearing your 65 | ideas, and I'll do my best to communicate my philosophy regardless of if I 66 | greenlight your feature or not. 67 | 68 | ## Cosmetic Changes 69 | 70 | Please do not open PRs for cosmetic changes to the codebase if we haven't had a 71 | prior discussion about it. Cosmetic changes include things like whitespace, 72 | commenting/documentation, naming conventions, and other aspects of the codebase 73 | that would not affect end users if changed. I am cautiously open to receiving 74 | respectful feedback about my general coding style, but I'm not generally 75 | interested in unsolicited style critiques, or for coding conventions dictated to 76 | me. Thank you in advance for your understanding! 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.egg-info 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 endorphant@tilde.town 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.html: -------------------------------------------------------------------------------- 1 |

a command-line based blogging platform running on tilde.town

2 | 3 |

ttbp stands for “tilde.town blogging platform”, the original working name for 4 | this project.

5 | 6 |

ttbp main menu screenshot

7 | 8 |

ttbp entries view screenshot

9 | 10 |

ttbp compose view screenshot

11 | 12 |

ttbp runs from the command line, providing a hub for writing personal blog 13 | posts and reading posts written by other users of tilde.town. it’s a little bit 14 | like livejournal or dreamwidth or tumblr. you can opt to publish your posts to a 15 | public html file hosted on your tilde page, to tilde.town’s gopher server, or 16 | keep all your entries private to the tilde.town server.

17 | 18 |

to use, run feels while logged in to tilde.town

19 | 20 |

this is a project that runs on tilde.town, so all users of this program are 21 | expected to operate under the tilde.town code of 22 | conduct. content/personal issues should be 23 | worked out according to the CoC, with support from the administrative 24 | team if needed.

25 | 26 |

QUICK START

27 | 28 |

no coding or html experience is necessary to get started. just log in to your 29 | tilde.town account and enter:

30 | 31 |

feels

32 | 33 |

ttbp will ask you a few questions to get you started. after that, writing and 34 | reading entries all happen within the program.

35 | 36 |

that’s it!

37 | 38 |

support

39 | 40 |

if you’re having trouble getting started, or run into program errors or strange 41 | behavior, please send internal tilde.town mail to ~endorphant and i’ll try to 42 | sort things out!

43 | 44 |

there’s also a function from the main menu that lets you send feedback/inquiries 45 | to me directly; this uses internal tilde.town mail, which is what i’ll respond 46 | to.

47 | 48 |

writing entries

49 | 50 |

entries are recorded as plaintext files in your ~/.ttbp/entries directory. 51 | ttbp will use your selected editor to open and write files; each day is its 52 | own entry, like a diary page. at midnight for whatever timezone you’ve set for 53 | your user account on tilde.town, you’ll get a fresh entry. if you don’t write 54 | any feels on a particular day, no entries will show up there.

55 | 56 |

when you save and quit the text editor, your entry will automatically propagate 57 | to the global feels list; if you’ve opted to publish your feels to html/gopher, 58 | those files will update immediately. you can always go back to the current day’s 59 | entry and edit/add as you’d like, but older entries will not be available for 60 | editing from ttbp.

61 | 62 |

(since files are just stored as plaintext in your directory, it’s possible to 63 | edit and move old entries directly from the command line. however, changing old 64 | entries might cause strange things to happen with timestamps. the main program 65 | looks at the filename first for setting the date, then the last modified time to 66 | sort recent posts. it expects YYYMMDD.txt as the filename; anything else won’t 67 | show up as a valid entry. yes, this means you can post things out of date order 68 | by creating files with any date you want.)

69 | 70 |

general entry-writing notes

71 | 72 | 79 | 80 | 81 |

reading other feels

82 | 83 |

the browse global feels feature shows the ten most recent entries that anyone 84 | has written on ttbp. this list is only accessible from within tilde.town, 85 | although individual entries may be posted to html or gopher.

86 | 87 |

you can also pull up a list of a single user’s feels through check out your 88 | neighbors, which displays all users who are writing on ttbp based on their 89 | most recently updated entry, and a link to their public html blog if they’ve 90 | opted to publish their posts.

91 | 92 |

please note! entries written on ttbp should be considered sensitive, 93 | private information, even if a particular user is publishing entries in a 94 | world-viewable way! please be respectful about having access to other people’s 95 | feels, and do not copy/repeat any information without getting their explicit 96 | permission. tilde.town operates on a high level of mutual trust, and ttbp is 97 | designed to give individuals control over their content.

98 | 99 |

subscribing to users

100 | 101 |

the visit your subscriptions feature lets you view recent entries from a list 102 | of users you’ve subscribed to, as well as manage your subscription list. your 103 | list is saved to ~/.ttbp/config/subs, which gets automatically updated when 104 | you add or remove subscriptions from ttbp.

105 | 106 |

your subscription list is private; this means no one other than you can see who 107 | you’re following. subscription view only shows the 50 most recent entries from 108 | your subscribe list; if you want to see more entries from an individual, you 109 | can navigate to their entries in `check out your neighbors from the main 110 | menu.

111 | 112 |

privacy

113 | 114 |

when you start your ttbp, you have the option of publishing or not publishing 115 | your blog.

116 | 117 |

if you opt to not publish, your entires will never be accessible from outside of 118 | the tilde.town network; other tilde.town users will still be able to read your 119 | entries through the ttbp interface, or by directly accessing your 120 | ~/.ttbp/entries directory.

121 | 122 |

if you want to further protect your entries, you can chmod 700 your entries 123 | directory.

124 | 125 |

if you opt to publish, the program creates a directory ~/.ttbp/www where it 126 | stores all html files it generates, and symlinks this from your ~/public_html 127 | with your chosen blog directory. your blog will also be listed on the main ttbp 128 | page.

129 | 130 |

you can also opt to publish to gopher, and the program will automatically 131 | generate a gophermap of your feels.

132 | 133 |

you can set publishing status on individual entries, or bury individual feels; 134 | see “data management” below for details.

135 | 136 |

data management

137 | 138 |

the manage your feels menu provides several tools for organizing your feels. 139 | these are all actions you can perform manually from the command line, but doing 140 | them from within the program can help keep your files properly linked up.

141 | 142 | 185 | 186 | 187 |

settings

188 | 189 |

the settings menu lets you change specific options for handling your feels and 190 | using the interface.

191 | 192 | 203 | 204 | 205 |

changing your page layout

206 | 207 |

you can modify how your blog looks by editing the stylesheet or header and 208 | footer files. the program sets you up with basic default. if you break your page 209 | somehow, you can force the program to regenerate your configuration by deleting 210 | your ~/.ttbp directory entirely. you might want to back up your 211 | ~/.ttbp/entries directory before you do this.

212 | 213 | 224 | 225 | 226 |

general tips/troubleshooting

227 | 228 | 236 | 237 | 238 |

future features

239 | 240 |

these are a few ideas being kicked around, or under active development:

241 | 242 | 247 | 248 | 249 |

other ideas are listed on github as 250 | upcoming features or feature requests!

251 | 252 |

dependencies

253 | 254 |

(this section is only relevant if you plan on forking the repo and running an 255 | instance of this yourself)

256 | 257 | 262 | 263 | 264 |

contributing

265 | 266 |

please check out my contributor 267 | guidelines 268 | on github if you’d like to get involved with development!

269 | 270 |

if you find any bugs or strange behavior, please message me locally on tildemail 271 | or open a github issue and i’ll get back to you as soon as i can.

272 | 273 |

if you’re interested in helping with the code, please drop me some tildemail!

274 | 275 |

i accept tips for development work on 276 | liberapay

277 | 278 |

contributor shout-outs

279 | 280 |

thanks to:

281 | 282 | 287 | 288 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *a command-line based blogging platform running on tilde.town* 2 | 3 | `ttbp` stands for "tilde.town blogging platform", the original working name for 4 | this project. 5 | 6 | ![ttbp main menu screenshot](http://tilde.town/~endorphant/ttbp/screenshots/ttbp-main.png) 7 | 8 | ![ttbp entries view screenshot](http://tilde.town/~endorphant/ttbp/screenshots/ttbp-entries.png) 9 | 10 | ![ttbp compose view screenshot](http://tilde.town/~endorphant/ttbp/screenshots/ttbp-compose.png) 11 | 12 | `ttbp` runs from the command line, providing a hub for writing personal blog 13 | posts and reading posts written by other users of tilde.town. it's a little bit 14 | like livejournal or dreamwidth or tumblr. you can opt to publish your posts to a 15 | public html file hosted on your tilde page, to tilde.town's gopher server, or 16 | keep all your entries private to the tilde.town server. 17 | 18 | to use, run `feels` while logged in to tilde.town 19 | 20 | this is a project that runs on tilde.town, so all users of this program are 21 | expected to operate under the tilde.town [code of 22 | conduct](http://tilde.town/wiki/conduct.html). content/personal issues should be 23 | worked out according to the CoC, with support from the [administrative 24 | team](http://tilde.town/wiki/administration/index.html) if needed. 25 | 26 | 27 | ### QUICK START 28 | 29 | no coding or html experience is necessary to get started. just log in to your 30 | tilde.town account and enter: 31 | 32 | `feels` 33 | 34 | ttbp will ask you a few questions to get you started. after that, writing and 35 | reading entries all happen within the program. 36 | 37 | that's it! 38 | 39 | ### support 40 | 41 | if you're having trouble getting started, or run into program errors or strange 42 | behavior, please send internal tilde.town mail to ~endorphant and i'll try to 43 | sort things out! 44 | 45 | there's also a function from the main menu that lets you send feedback/inquiries 46 | to me directly; this uses internal tilde.town mail, which is what i'll respond 47 | to. 48 | 49 | ### writing entries 50 | 51 | entries are recorded as plaintext files in your `~/.ttbp/entries` directory. 52 | `ttbp` will use your selected editor to open and write files; each day is its 53 | own entry, like a diary page. at midnight for whatever timezone you've set for 54 | your user account on tilde.town, you'll get a fresh entry. if you don't write 55 | any feels on a particular day, no entries will show up there. 56 | 57 | when you save and quit the text editor, your entry will automatically propagate 58 | to the global feels list; if you've opted to publish your feels to html/gopher, 59 | those files will update immediately. you can always go back to the current day's 60 | entry and edit/add as you'd like, but older entries will not be available for 61 | editing from `ttbp`. 62 | 63 | *(since files are just stored as plaintext in your directory, it's possible to 64 | edit and move old entries directly from the command line. however, changing old 65 | entries might cause strange things to happen with timestamps. the main program 66 | looks at the filename first for setting the date, then the last modified time to 67 | sort recent posts. it expects YYYMMDD.txt as the filename; anything else won't 68 | show up as a valid entry. yes, this means you can post things out of date order 69 | by creating files with any date you want.)* 70 | 71 | #### general entry-writing notes 72 | 73 | * you can use [markdown](https://daringfireball.net/projects/markdown/syntax) 74 | * you can use html 75 | * you can also put things between `` to have them show up 76 | in the feed but not render in a browser (but people can still read 77 | them with view-source) 78 | 79 | ### reading other feels 80 | 81 | the `browse global feels` feature shows the ten most recent entries that anyone 82 | has written on ttbp. this list is only accessible from within tilde.town, 83 | although individual entries may be posted to html or gopher. 84 | 85 | you can also pull up a list of a single user's feels through `check out your 86 | neighbors`, which displays all users who are writing on `ttbp` based on their 87 | most recently updated entry, and a link to their public html blog if they've 88 | opted to publish their posts. 89 | 90 | **please note!** entries written on `ttbp` should be considered sensitive, 91 | private information, even if a particular user is publishing entries in a 92 | world-viewable way! please be respectful about having access to other people's 93 | feels, and do not copy/repeat any information without getting their explicit 94 | permission. tilde.town operates on a high level of mutual trust, and `ttbp` is 95 | designed to give individuals control over their content. 96 | 97 | ### subscribing to users 98 | 99 | the `visit your subscriptions` feature lets you view recent entries from a list 100 | of users you've subscribed to, as well as manage your subscription list. your 101 | list is saved to `~/.ttbp/config/subs`, which gets automatically updated when 102 | you add or remove subscriptions from ttbp. 103 | 104 | your subscription list is private; this means no one other than you can see who 105 | you're following. subscription view only shows the 50 most recent entries from 106 | your subscribe list; if you want to see more entries from an individual, you 107 | can navigate to their entries in `check out your neighbors from the main 108 | menu. 109 | 110 | ### privacy 111 | 112 | when you start your ttbp, you have the option of publishing or not publishing 113 | your blog. 114 | 115 | if you opt to not publish, your entires will never be accessible from outside of 116 | the tilde.town network; other tilde.town users will still be able to read your 117 | entries through the ttbp interface, or by directly accessing your 118 | `~/.ttbp/entries` directory. 119 | 120 | if you want to further protect your entries, you can `chmod 700` your entries 121 | directory. 122 | 123 | if you opt to publish, the program creates a directory `~/.ttbp/www` where it 124 | stores all html files it generates, and symlinks this from your `~/public_html` 125 | with your chosen blog directory. your blog will also be listed on the [main ttbp 126 | page](https://tilde.town/~endorphant/ttbp). 127 | 128 | you can also opt to publish to gopher, and the program will automatically 129 | generate a gophermap of your feels. 130 | 131 | you can set publishing status on individual entries, or bury individual feels; 132 | see "data management" below for details. 133 | 134 | ### data management 135 | 136 | the `manage your feels` menu provides several tools for organizing your feels. 137 | these are all actions you can perform manually from the command line, but doing 138 | them from within the program can help keep your files properly linked up. 139 | 140 | * **read over feels**--a list of all your entries, which you can open and 141 | read like any other feel 142 | * **modify feels publishing**--this lets you toggle privacy on individual 143 | posts. entries marked `(nopub)` will not get written to html or gopher, 144 | and toggling them from this menu will immediately publish or unpublish 145 | that entry (if you're not publishing your posts at all, these settings 146 | won't matter, since your feels will never show up outside of tilde.town) 147 | * **backup your feels**--makes a .tar.gz of all your entries, saving one 148 | copy to `~/.ttbp/backups/` with the current date, and a second copy to 149 | your home directory for safekeeping. 150 | * **import a feels backup**--unpacks a backup file into your current feels 151 | list. this tool checks the `~/.ttbp/backups` directory for archives, and 152 | expects a file created by the above backup utility. if it detects any file 153 | collisions, it will preserve your current live copy and leave the backup 154 | verison in a temp directory, and notify you that this happened. also, any 155 | entries that were previously marked as `(nopub)` will retain their nopub 156 | status. 157 | * **bury some feels**--hides individual feels from viewing; entries are 158 | moved to `~/.ttbp/buried` (and marked with a unique timestamp to prevent 159 | file collision) with permissions set to 600, meaning no one except you 160 | will be able to open that file. these entries are also hidden from your 161 | own view from `read over feels`, and you'll have to open the files from 162 | the command line if you want to see them. this is intended to be a 163 | permament action, so you'll be asked to type the entry date once to load 164 | the feel, then shown a preview of that feel, and then type the date again 165 | to confirm burying. 166 | * **delete feels by day**--*permanently removes individual entries*, 167 | including deleting published html/gopher files if needed. this action is 168 | not recoverable, unless you have a backup to restore; you'll be asked to 169 | type the entry date once to load the feel, then shown a preview of that 170 | feel, and then type the date again to confirm deletion. 171 | * **purge all feels**--*permanently removes all feels*, including deleting 172 | all published html/gopher files if needed. this action is not recoverable, 173 | unless you have a backup to restore. you'll be asked to type a 174 | one-time-use purge code to confirm this action. 175 | * **wipe feels account**--*permanently removes all data associated with 176 | feels*, including deleting any published hmtl/gopher files and removing 177 | your `~/.ttbp` directory. any backups that you have in `~/.ttbp/backups` 178 | will also be deleted with this action (which is why the backup function 179 | makes a second copy for safekeeping in your home directory). you will no 180 | longer show up in any lists as a user. 181 | 182 | ### settings 183 | 184 | the settings menu lets you change specific options for handling your feels and 185 | using the interface. 186 | 187 | * **editor**--set your text editor 188 | * **gopher**--opt in or out of automatically posting to gopher 189 | * **post as nopub**--set whether posts default to being published or not 190 | published (if you're not publishing your feels, this doesn't matter) 191 | * **publish dir**--set the directory under you `public_html` where feels will be 192 | published (if you're not publishing your feels, this defaults to `None`) 193 | * **publishing**--opt in or out of automatically publishing entries to a 194 | world-readable html page 195 | * **rainbows**--opt in or out of having multicolored menu text 196 | 197 | ### changing your page layout 198 | 199 | you can modify how your blog looks by editing the stylesheet or header and 200 | footer files. the program sets you up with basic default. if you break your page 201 | somehow, you can force the program to regenerate your configuration by deleting 202 | your ~/.ttbp directory entirely. **you might want to back up your 203 | ~/.ttbp/entries directory before you do this.** 204 | 205 | * to modify your stylesheet, edit your ~/.ttbp/config/style.css 206 | * to modify the page header, edit your ~/.ttbp/config/header.txt 207 | * there's a place marked off in the default header where you can safely put 208 | custom HTML elements! 209 | * to modify the page footer, edit your ~/.ttbp/config/footer.txt 210 | 211 | ### general tips/troubleshooting 212 | 213 | * if the date looks like it's ahead or behind, it's because you haven't set 214 | your local timezone yet. here are some 215 | [timezone setting instructions](http://www.cyberciti.biz/faq/linux-unix-set-tz-environment-variable/) 216 | * the feels burying tool will effectively clear your post for the day; you can 217 | use this feature to start a fresh entry on a particular day by burying the 218 | current day's feels and then editing a new file 219 | 220 | ### future features 221 | 222 | these are a few ideas being kicked around, or under active development: 223 | 224 | * stylesheet/theme selector 225 | * better entry display within ttbp (currently just offloads to `less`) 226 | * buried feels browser 227 | 228 | other ideas are listed on github as 229 | [upcoming features](https://github.com/modgethanc/ttbp/issues?q=is%3Aissue+is%3Aopen+label%3A"upcoming+features") or [feature requests](https://github.com/modgethanc/ttbp/issues?q=is%3Aissue+is%3Aopen+label%3A"feature+request")! 230 | 231 | ### dependencies 232 | 233 | (this section is only relevant if you plan on forking the repo and running an 234 | instance of this yourself) 235 | 236 | * [mistune](https://pypi.python.org/pypi/mistune) 237 | * [inflect](https://pypi.python.org/pypi/inflect) 238 | * [six](https://pypi.python.org/pypi/six) 239 | 240 | ### contributing 241 | 242 | please check out my [contributor 243 | guidelines](https://github.com/modgethanc/ttbp/blob/master/.github/CONTRIBUTING.md) 244 | on github if you'd like to get involved with development! 245 | 246 | if you find any bugs or strange behavior, please message me locally on tildemail 247 | or open a github issue and i'll get back to you as soon as i can. 248 | 249 | if you're interested in helping with the code, please drop me some tildemail! 250 | 251 | i accept tips for development work on 252 | [liberapay](https://liberapay.com/modgethanc) 253 | 254 | ### contributor shout-outs 255 | 256 | thanks to: 257 | 258 | * ~vilmibm, packaging help and gopher support 259 | * ~sanqui, the bug swatter 260 | * ~sinacutie, for css updates 261 | * ~epicmorphism, for fixing pagination scrolling 262 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | TO-DO: 2 | (near future features) 3 | 4 | -cw feature 5 | -account deletion/export 6 | 7 | (possible/far future features) 8 | 9 | -stylesheet selector 10 | -and make a couple more custom stylesheets 11 | -replying to entries?? 12 | -direct mail to author 13 | -make a nicer entry view wrapper 14 | -maybe with a box? and a nicer feed view? 15 | -#hashtags 16 | -command line flags 17 | -shortcut to most recent feels, writing entry, seeing own entry 18 | -motd 19 | -rss 20 | -multiple posts per day 21 | 22 | ------ 23 | 24 | CHANGELOG: 25 | ver 0.11.0 26 | -added rainbow menus 27 | -added pub/nopub selector 28 | -fixed pagination for 2 page lists 29 | -fixing a bunch of things that broke in settings update 30 | -announcing centralfeels 31 | -general code cleanup pass 32 | 33 | ver 0.10.1 34 | -gopher support (with help from ~vilmibm) 35 | -fixed settings menu 36 | 37 | ver 0.9.3 (by ~vilmibm) 38 | -packaging 39 | -easier to contribute to 40 | 41 | ver 0.9.2 42 | -paginated scrolling 43 | 44 | ver 0.9.1 45 | -graffiti wall!!! 46 | 47 | ver 0.9.0 48 | -documentation browser 49 | -reading neighbor feels lists 50 | -overall code scrubbing and documentation 51 | 52 | ver 0.8.7 53 | -fixing publish/nopub option setting from patcher 54 | 55 | ver 0.8.6; mostly structural changes 56 | -a freaking patch utility?! yes, there's a patch utility now 57 | -settings now has an option to publish or not publish your blog to 58 | public_html 59 | -changing your publish directory actually moves files correctly 60 | -html files are now stored at .ttbp/www, and the public_html path just 61 | symlinks there 62 | -style.css now lives in .ttbp/config, and is symlinked from www 63 | 64 | ver 0.8.5 65 | -publish feels directory to ~endorphant/ttbp/index.html 66 | -(beta only) colorized menus 67 | 68 | ver 0.8.0 69 | -markdown parsing for entries 70 | 71 | ver 0.7.5 72 | -COLORFUL BANNER 73 | -backend stuff; feedback uses sendmail, some code cleanup 74 | 75 | ver 0.7 76 | -fixed program crashing? maybe? 77 | -added credits 78 | -added filename validator 79 | 80 | ver 0.6.5 81 | -renamed FEELS ENGINE 82 | -changed neighbor view to show last update time instead of feels count 83 | 84 | ver 0.6 85 | -improving exit handling 86 | -sort neighbor view by most recently updated 87 | -capture input from recording feels screen 88 | 89 | ver 0.5 90 | -added reading own entries 91 | -added reading 10 most recent global entries 92 | 93 | ver 0.4 94 | -added list of all users with a ttbp 95 | 96 | ver 0.3 97 | -user setup, change settings 98 | 99 | ver 0.2 100 | -feedback form, post entry 101 | 102 | ver 0.1 103 | -acknowledges user 104 | -------------------------------------------------------------------------------- /doc/manual.html: -------------------------------------------------------------------------------- 1 |

FEELS MANUAL

2 |

ttbp stands for "tilde.town blogging platform", the original working name for 3 | this project. the complete codebase is available on 4 | github.

5 |

ttbp runs from the command line, providing a hub for writing personal blog 6 | posts and reading posts written by other users of tilde.town. it's a little bit 7 | like livejournal or dreamwidth or tumblr. you can opt to publish your posts to a 8 | public html file hosted on your tilde page, to tilde.town's gopher server, or 9 | keep all your entries private to the tilde.town server.

10 |

this is a project that runs on tilde.town, so all users of this program are 11 | expected to operate under the tilde.town code of 12 | conduct. content/personal issues should be 13 | worked out according to the CoC, with support from the administrative 14 | team if needed.

15 |

support

16 |

if you're having trouble getting started, or run into program errors or strange 17 | behavior, please send internal tilde.town mail to ~endorphant and i'll try to 18 | sort things out!

19 |

there's also a function from the main menu that lets you send feedback/inquiries 20 | to me directly; this uses internal tilde.town mail, which is what i'll respond 21 | to.

22 |

writing entries

23 |

entries are recorded as plaintext files in your ~/.ttbp/entries directory. 24 | ttbp will use your selected editor to open and write files; each day is its 25 | own entry, like a diary page. at midnight for whatever timezone you've set for 26 | your user account on tilde.town, you'll get a fresh entry. if you don't write 27 | any feels on a particular day, no entries will show up there.

28 |

when you save and quit the text editor, your entry will automatically propagate 29 | to the global feels list; if you've opted to publish your feels to html/gopher, 30 | those files will update immediately. you can always go back to the current day's 31 | entry and edit/add as you'd like, but older entries will not be available for 32 | editing from ttbp.

33 |

(since files are just stored as plaintext in your directory, it's possible to 34 | edit and move old entries directly from the command line. however, changing old 35 | entries might cause strange things to happen with timestamps. the main program 36 | looks at the filename first for setting the date, then the last modified time to 37 | sort recent posts. it expects YYYMMDD.txt as the filename; anything else won't 38 | show up as a valid entry. yes, this means you can post things out of date order 39 | by creating files with any date you want.)

40 |

general entry-writing notes

41 | 48 |

reading other feels

49 |

the browse global feels feature shows the ten most recent entries that anyone 50 | has written on ttbp. this list is only accessible from within tilde.town, 51 | although individual entries may be posted to html or gopher.

52 |

you can also pull up a list of a single user's feels through check out your 53 | neighbors, which displays all users who are writing on ttbp based on their 54 | most recently updated entry, and a link to their public html blog if they've 55 | opted to publish their posts.

56 |

please note! entries written on ttbp should be considered sensitive, 57 | private information, even if a particular user is publishing entries in a 58 | world-viewable way! please be respectful about having access to other people's 59 | feels, and do not copy/repeat any information without getting their explicit 60 | permission. tilde.town operates on a high level of mutual trust, and ttbp is 61 | designed to give individuals control over their content.

62 |

privacy

63 |

when you start your ttbp, you have the option of publishing or not publishing 64 | your blog.

65 |

if you opt to not publish, your entires will never be accessible from outside of 66 | the tilde.town network; other tilde.town users will still be able to read your 67 | entries through the ttbp interface, or by directly accessing your 68 | ~/.ttbp/entries directory.

69 |

if you want to further protect your entries, you can chmod 700 your entries 70 | directory.

71 |

if you opt to publish, the program creates a directory ~/.ttbp/www where it 72 | stores all html files it generates, and symlinks this from your ~/public_html 73 | with your chosen blog directory. your blog will also be listed on the main ttbp 74 | page.

75 |

you can also opt to publish to gopher, and the program will automatically 76 | generate a gophermap of your feels.

77 |

you can set publishing status on individual entries, or bury individual feels; 78 | see "data management" below for details.

79 |

data management

80 |

the manage your feels menu provides several tools for organizing your feels. 81 | these are all actions you can perform manually from the command line, but doing 82 | them from within the program can help keep your files properly linked up.

83 | 126 |

settings

127 |

the settings menu lets you change specific options for handling your feels and 128 | using the interface.

129 | 140 |

changing your page layout

141 |

you can modify how your blog looks by editing the stylesheet or header and 142 | footer files. the program sets you up with basic default. if you break your page 143 | somehow, you can force the program to regenerate your configuration by deleting 144 | your ~/.ttbp directory entirely. you might want to back up your 145 | ~/.ttbp/entries directory before you do this.

146 | 153 |

general tips/troubleshooting

154 | 162 |

future features

163 |

these are a few ideas being kicked around, or under active development:

164 | 169 |

other ideas are listed on github as 170 | upcoming features or feature requests!

-------------------------------------------------------------------------------- /doc/manual.md: -------------------------------------------------------------------------------- 1 | # FEELS MANUAL # 2 | 3 | `ttbp` stands for "tilde.town blogging platform", the original working name for 4 | this project. the complete codebase is available on 5 | [github](https://github.com/modgethanc/ttbp). 6 | 7 | `ttbp` runs from the command line, providing a hub for writing personal blog 8 | posts and reading posts written by other users of tilde.town. it's a little bit 9 | like livejournal or dreamwidth or tumblr. you can opt to publish your posts to a 10 | public html file hosted on your tilde page, to tilde.town's gopher server, or 11 | keep all your entries private to the tilde.town server. 12 | 13 | this is a project that runs on tilde.town, so all users of this program are 14 | expected to operate under the tilde.town [code of 15 | conduct](http://tilde.town/wiki/conduct.html). content/personal issues should be 16 | worked out according to the CoC, with support from the [administrative 17 | team](http://tilde.town/wiki/administration/index.html) if needed. 18 | 19 | ### support 20 | 21 | if you're having trouble getting started, or run into program errors or strange 22 | behavior, please send internal tilde.town mail to ~endorphant and i'll try to 23 | sort things out! 24 | 25 | there's also a function from the main menu that lets you send feedback/inquiries 26 | to me directly; this uses internal tilde.town mail, which is what i'll respond 27 | to. 28 | 29 | ### writing entries 30 | 31 | entries are recorded as plaintext files in your `~/.ttbp/entries` directory. 32 | `ttbp` will use your selected editor to open and write files; each day is its 33 | own entry, like a diary page. at midnight for whatever timezone you've set for 34 | your user account on tilde.town, you'll get a fresh entry. if you don't write 35 | any feels on a particular day, no entries will show up there. 36 | 37 | when you save and quit the text editor, your entry will automatically propagate 38 | to the global feels list; if you've opted to publish your feels to html/gopher, 39 | those files will update immediately. you can always go back to the current day's 40 | entry and edit/add as you'd like, but older entries will not be available for 41 | editing from `ttbp`. 42 | 43 | *(since files are just stored as plaintext in your directory, it's possible to 44 | edit and move old entries directly from the command line. however, changing old 45 | entries might cause strange things to happen with timestamps. the main program 46 | looks at the filename first for setting the date, then the last modified time to 47 | sort recent posts. it expects YYYMMDD.txt as the filename; anything else won't 48 | show up as a valid entry. yes, this means you can post things out of date order 49 | by creating files with any date you want.)* 50 | 51 | #### general entry-writing notes 52 | 53 | * you can use [markdown](https://daringfireball.net/projects/markdown/syntax) 54 | * you can use html 55 | * you can also put things between `` to have them show up 56 | in the feed but not render in a browser (but people can still read them with 57 | view-source) 58 | 59 | ### reading other feels 60 | 61 | the `browse global feels` feature shows the ten most recent entries that anyone 62 | has written on ttbp. this list is only accessible from within tilde.town, 63 | although individual entries may be posted to html or gopher. 64 | 65 | you can also pull up a list of a single user's feels through `check out your 66 | neighbors`, which displays all users who are writing on `ttbp` based on their 67 | most recently updated entry, and a link to their public html blog if they've 68 | opted to publish their posts. 69 | 70 | **please note!** entries written on `ttbp` should be considered sensitive, 71 | private information, even if a particular user is publishing entries in a 72 | world-viewable way! please be respectful about having access to other people's 73 | feels, and do not copy/repeat any information without getting their explicit 74 | permission. tilde.town operates on a high level of mutual trust, and `ttbp` is 75 | designed to give individuals control over their content. 76 | 77 | ### privacy 78 | 79 | when you start your ttbp, you have the option of publishing or not publishing 80 | your blog. 81 | 82 | if you opt to not publish, your entires will never be accessible from outside of 83 | the tilde.town network; other tilde.town users will still be able to read your 84 | entries through the ttbp interface, or by directly accessing your 85 | `~/.ttbp/entries` directory. 86 | 87 | if you want to further protect your entries, you can `chmod 700` your entries 88 | directory. 89 | 90 | if you opt to publish, the program creates a directory `~/.ttbp/www` where it 91 | stores all html files it generates, and symlinks this from your `~/public_html` 92 | with your chosen blog directory. your blog will also be listed on the [main ttbp 93 | page](https://tilde.town/~endorphant/ttbp). 94 | 95 | you can also opt to publish to gopher, and the program will automatically 96 | generate a gophermap of your feels. 97 | 98 | you can set publishing status on individual entries, or bury individual feels; 99 | see "data management" below for details. 100 | 101 | ### data management 102 | 103 | the `manage your feels` menu provides several tools for organizing your feels. 104 | these are all actions you can perform manually from the command line, but doing 105 | them from within the program can help keep your files properly linked up. 106 | 107 | * **read over feels**--a list of all your entries, which you can open and 108 | read like any other feel 109 | * **modify feels publishing**--this lets you toggle privacy on individual 110 | posts. entries marked `(nopub)` will not get written to html or gopher, 111 | and toggling them from this menu will immediately publish or unpublish 112 | that entry (if you're not publishing your posts at all, these settings 113 | won't matter, since your feels will never show up outside of tilde.town) 114 | * **backup your feels**--makes a .tar.gz of all your entries, saving one 115 | copy to `~/.ttbp/backups/` with the current date, and a second copy to 116 | your home directory for safekeeping. 117 | * **import a feels backup**--unpacks a backup file into your current feels 118 | list. this tool checks the `~/.ttbp/backups` directory for archives, and 119 | expects a file created by the above backup utility. if it detects any file 120 | collisions, it will preserve your current live copy and leave the backup 121 | verison in a temp directory, and notify you that this happened. also, any 122 | entries that were previously marked as `(nopub)` will retain their nopub 123 | status. 124 | * **bury some feels**--hides individual feels from viewing; entries are 125 | moved to `~/.ttbp/buried` (and marked with a unique timestamp to prevent 126 | file collision) with permissions set to 600, meaning no one except you 127 | will be able to open that file. these entries are also hidden from your 128 | own view from `read over feels`, and you'll have to open the files from 129 | the command line if you want to see them. this is intended to be a 130 | permament action, so you'll be asked to type the entry date once to load 131 | the feel, then shown a preview of that feel, and then type the date again 132 | to confirm burying. 133 | * **delete feels by day**--*permanently removes individual entries*, 134 | including deleting published html/gopher files if needed. this action is 135 | not recoverable, unless you have a backup to restore; you'll be asked to 136 | type the entry date once to load the feel, then shown a preview of that 137 | feel, and then type the date again to confirm deletion. 138 | * **purge all feels**--*permanently removes all feels*, including deleting 139 | all published html/gopher files if needed. this action is not recoverable, 140 | unless you have a backup to restore. you'll be asked to type a 141 | one-time-use purge code to confirm this action. 142 | * **wipe feels account**--*permanently removes all data associated with 143 | feels*, including deleting any published hmtl/gopher files and removing 144 | your `~/.ttbp` directory. any backups that you have in `~/.ttbp/backups` 145 | will also be deleted with this action (which is why the backup function 146 | makes a second copy for safekeeping in your home directory). you will no 147 | longer show up in any lists as a user. 148 | 149 | ### settings 150 | 151 | the settings menu lets you change specific options for handling your feels and 152 | using the interface. 153 | 154 | * **editor**--set your text editor 155 | * **gopher**--opt in or out of automatically posting to gopher 156 | * **post as nopub**--set whether posts default to being published or not 157 | published (if you're not publishing your feels, this doesn't matter) 158 | * **publish dir**--set the directory under you `public_html` where feels will be 159 | published (if you're not publishing your feels, this defaults to `None`) 160 | * **publishing**--opt in or out of automatically publishing entries to a 161 | world-readable html page 162 | * **rainbows**--opt in or out of having multicolored menu text 163 | 164 | ### changing your page layout 165 | 166 | you can modify how your blog looks by editing the stylesheet or header and 167 | footer files. the program sets you up with basic default. if you break your page 168 | somehow, you can force the program to regenerate your configuration by deleting 169 | your ~/.ttbp directory entirely. **you might want to back up your 170 | ~/.ttbp/entries directory before you do this.** 171 | 172 | * to modify your stylesheet, edit your ~/.ttbp/config/style.css 173 | * to modify the page header, edit your ~/.ttbp/config/header.txt 174 | * there's a place marked off in the default header where you can safely put 175 | custom HTML elements! 176 | * to modify the page footer, edit your ~/.ttbp/config/footer.txt 177 | 178 | ### general tips/troubleshooting 179 | 180 | * if the date looks like it's ahead or behind, it's because you haven't set 181 | your local timezone yet. here are some 182 | [timezone setting instructions](http://www.cyberciti.biz/faq/linux-unix-set-tz-environment-variable/) 183 | * the feels burying tool will effectively clear your post for the day; you can 184 | use this feature to start a fresh entry on a particular day by burying the 185 | current day's feels and then editing a new file 186 | 187 | ### future features 188 | 189 | these are a few ideas being kicked around, or under active development: 190 | 191 | * stylesheet/theme selector 192 | * better entry display within ttbp (currently just offloads to `less`) 193 | * buried feels browser 194 | 195 | other ideas are listed on github as 196 | [upcoming features](https://github.com/modgethanc/ttbp/issues?q=is%3Aissue+is%3Aopen+label%3A"upcoming+features") or [feature requests](https://github.com/modgethanc/ttbp/issues?q=is%3Aissue+is%3Aopen+label%3A"feature+request")! 197 | -------------------------------------------------------------------------------- /screenshots/ttbp-compose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modgethanc/ttbp/0aaedaff5a2738c11cfdaa29329533b9f4efaf28/screenshots/ttbp-compose.png -------------------------------------------------------------------------------- /screenshots/ttbp-entries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modgethanc/ttbp/0aaedaff5a2738c11cfdaa29329533b9f4efaf28/screenshots/ttbp-entries.png -------------------------------------------------------------------------------- /screenshots/ttbp-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modgethanc/ttbp/0aaedaff5a2738c11cfdaa29329533b9f4efaf28/screenshots/ttbp-main.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='ttbp', 7 | version='0.12.2', 8 | description='command line social blogging tool used on tilde.town', 9 | url='https://github.com/modgethanc/ttbp', 10 | author='~endorphant', 11 | author_email='endorphant@tilde.town', 12 | license='MIT', 13 | classifiers=[ 14 | 'Topic :: Artistic Software', 15 | 'License :: OSI Approved :: MIT License', 16 | ], 17 | keywords='blog', 18 | packages=['ttbp'], 19 | install_requires = [ 20 | 'inflect==0.2.5', 21 | 'mistune==0.8.1', 22 | 'colorama==0.3.9', 23 | 'six' 24 | ], 25 | include_package_data = True, 26 | entry_points = { 27 | 'console_scripts': [ 28 | 'feels = ttbp.ttbp:main', 29 | 'ttbp = ttbp.ttbp:main', 30 | ] 31 | }, 32 | ) 33 | -------------------------------------------------------------------------------- /ttbp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modgethanc/ttbp/0aaedaff5a2738c11cfdaa29329533b9f4efaf28/ttbp/__init__.py -------------------------------------------------------------------------------- /ttbp/chatter.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import random 4 | 5 | DEFAULT_LANG = { 6 | "greet":[ 7 | "hi", 8 | "hey", 9 | "howdy", 10 | "good morning", 11 | "good afternoon", 12 | "good day", 13 | "good evening", 14 | "welcome back", 15 | "nice to see you" 16 | ], 17 | "bye":[ 18 | "see you later, space cowboy", 19 | "bye, townie", 20 | "until next time, friend", 21 | "come back whenever" 22 | ], 23 | "friend":[ 24 | "friend", 25 | "pal", 26 | "buddy", 27 | "townie" 28 | ], 29 | "months":{ 30 | "01":"january", 31 | "02":"february", 32 | "03":"march", 33 | "04":"april", 34 | "05":"may", 35 | "06":"june", 36 | "07":"july", 37 | "08":"august", 38 | "09":"september", 39 | "10":"october", 40 | "11":"november", 41 | "12":"december" 42 | } 43 | } 44 | 45 | if os.path.exists("/home/endorphant/lib/python/chatterlib.json"): 46 | with open("/home/endorphant/lib/python/chatterlib.json", 'r') as f: 47 | LANG = json.load(f) 48 | else: 49 | LANG = DEFAULT_LANG 50 | 51 | def say(keyword): 52 | ''' 53 | takes a keyword and randomly returns from language dictionary to match that keyword 54 | 55 | returns None if keyword doesn't exist 56 | 57 | TODO: validate keyword? 58 | ''' 59 | 60 | return random.choice(LANG.get(keyword)) 61 | 62 | def month(num): 63 | ''' 64 | takes a MM and returns lovercase full name of that month 65 | 66 | TODO: validate num? 67 | ''' 68 | 69 | return LANG["months"].get(num) 70 | -------------------------------------------------------------------------------- /ttbp/config/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import os 3 | import sys 4 | import time 5 | 6 | from .. import util 7 | 8 | ## System config 9 | 10 | # We refer to some package files (ie .css stuff), so we save a reference to the 11 | # path. 12 | INSTALL_PATH = os.path.dirname(sys.modules['ttbp'].__file__) 13 | 14 | # We use this to store any persisted, global state. 15 | VAR = '/var/global/ttbp' 16 | VAR_WWW = os.path.join(VAR, 'www') 17 | 18 | if not os.path.isdir('/var/global'): 19 | raise Exception('bad system state: /var/global does not exist.') 20 | 21 | if not os.path.isdir(VAR): 22 | os.mkdir(VAR) 23 | 24 | if not os.path.isdir(VAR_WWW): 25 | os.mkdir(VAR_WWW) 26 | 27 | LIVE = 'https://tilde.town/~' 28 | FEEDBOX = "endorphant@tilde.town" 29 | USERFILE = os.path.join(VAR, "users.txt") 30 | GRAFF_DIR = os.path.join(VAR, "graffiti") 31 | WALL = os.path.join(GRAFF_DIR, "wall.txt") 32 | WALL_LOCK = os.path.join(GRAFF_DIR, ".lock") 33 | 34 | if not os.path.isdir(GRAFF_DIR): 35 | os.mkdir(GRAFF_DIR) 36 | 37 | ## Defaults 38 | 39 | DEFAULT_HEADER = ''' 40 | 41 | 42 | 43 | $USER on TTBP 44 | 45 | 46 | 47 |
48 |

~$USER@TTBP

49 |
50 | 51 |
52 | '''.lstrip() 53 | 54 | DEFAULT_FOOTER = ''' 55 |
56 | 57 | 58 | '''.lstrip() 59 | 60 | with open(os.path.join(INSTALL_PATH, 'config', 'defaults', 'style.css')) as f: 61 | DEFAULT_STYLE = f.read() 62 | 63 | 64 | ## User config 65 | 66 | USER = os.path.basename(os.path.expanduser('~')) 67 | USER_HOME = os.path.expanduser('~') 68 | PATH = os.path.join(USER_HOME, '.ttbp') 69 | PUBLIC = os.path.join(USER_HOME, 'public_html') 70 | WWW = os.path.join(PATH, 'www') 71 | GOPHER_ENTRIES = os.path.join(PATH, 'gopher') 72 | GOPHER_PATH = os.path.join(USER_HOME, 'public_gopher', 'feels') 73 | USER_CONFIG = os.path.join(PATH, 'config') 74 | TTBPRC = os.path.join(USER_CONFIG, 'ttbprc') 75 | MAIN_FEELS = os.path.join(PATH, 'entries') 76 | BURIED_FEELS = os.path.join(PATH, 'buried') 77 | NOPUB = os.path.join(USER_CONFIG, 'nopub') 78 | BACKUPS = os.path.join(PATH, 'backups') 79 | SUBS = os.path.join(USER_CONFIG, 'subs') 80 | 81 | ## UI 82 | 83 | BANNER = ''' 84 | ___________________________________________________________ 85 | | | 86 | | the tilde.town | 87 | | ____ ____ ____ _ ____ ____ _ _ ____ _ _ _ ____ | 88 | | |___ |___ |___ | [__ |___ |\ | | __ | |\ | |___ | 89 | | | |___ |___ |___ ___] |___ | \| |__] | | \| |___ | 90 | | ver 0.12.2 | 91 | |__________________________________________________________| 92 | '''.lstrip() 93 | # ~ u n s t a b l e e x p e r i m e n t a l b r a n c h ~ 94 | #'''.lstrip() 95 | 96 | ## page texts 97 | 98 | intro_prompt = """ 99 | i don't recognize you, stranger. let's make friends. 100 | 101 | the feels engine is an internal blogging platform on tilde.town. it assists you 102 | in recording your feels, giving you the option to publish to html or gopher, and 103 | read the feels of other users on tilde.town. 104 | 105 | press to set up an account, or to quit. 106 | """.lstrip() 107 | 108 | credits = """ 109 | ttbp was written for tilde.town by ~endorphant in python. the codebase is 110 | publicly available on github at https://github.com/modgethanc/ttbp 111 | 112 | tips for development are accepted at https://liberapay.com/modgethanc/ 113 | 114 | other contributors: 115 | ~vilmibm, packaging help and gopher support 116 | ~sanqui, the bug swatter 117 | ~sinacutie, for css updates 118 | 119 | if you have ideas for ttbp, you are welcome to contact me to discuss them; 120 | please send me tildemail or open a github issue. i am not a very experienced 121 | developer, and ttbp is one of my first public-facing projects, so i appreciate 122 | your patience while i learn how to be a better developer! 123 | 124 | i'd love to hear about your ideas and brainstorm about new features! 125 | 126 | thanks to everyone who reads, listens, writes, and feels.""" 127 | 128 | recording = """ 129 | feels will be recorded for today, {today}. 130 | 131 | if you've already started recording feels for this day, you 132 | can pick up where you left off. 133 | 134 | you can write your feels in plaintext, markdown, html, or a mixture of 135 | these. 136 | 137 | press to begin recording your feels in your chosen text 138 | editor. 139 | 140 | """.format(today=time.strftime("%d %B %Y")) 141 | 142 | bury_feels_prompt = """\ 143 | burying a feel removes it from view, including your own. buried feels are 144 | stashed in a private directory at: 145 | 146 | {buried_dir} 147 | 148 | you can visit your feels there from the command line, but no one else can view 149 | those files. 150 | 151 | (a buried feels browser is in the works; for now, you'll have to use the 152 | command line to view your buried feels) 153 | 154 | which day's feels do you want to bury? 155 | 156 | YYYYMMDD (or 'q' to cancel)> """.format(buried_dir=BURIED_FEELS) 157 | 158 | account_wipe_prompt = """\ 159 | warning! ! ! this action is irreversible!!! 160 | 161 | this tool will remove your entire presence from the feels engine. this includes 162 | all posts, settings, and published html/gopher feels. you will no longer be 163 | listed anywhere as a user here. 164 | 165 | there is no way for me to help you recover any part of your feels acccount. i 166 | respect your need to do this from time to time, so please be sure you're ready! 167 | 168 | i recommend that you make a backup of your feels and stash them somewhere safe, 169 | just in case a future version of you still wants to look them over.""" 170 | 171 | feels_purge_prompt = """\ 172 | warning! ! ! this action is irreversible!!! 173 | 174 | there is no way for me to help you recover your feels if you purge them all. i 175 | respect your need to do this from time to time, so please be sure you're ready! 176 | 177 | i recommend that you make a backup of your feels and stash them somewhere safe, 178 | just in case a future version of you still wants to look them over. 179 | """ 180 | 181 | mystery_error = """\ 182 | sorry, something went wrong! please try to address the error and try again. 183 | if you need help, ask in IRC or send mail to ~endorphant and we'll try to 184 | figure it out! 185 | """.lstrip() 186 | 187 | ## update announcements 188 | 189 | UPDATES = { 190 | "0.9.0": """ 191 | ver. 0.9.0 features: 192 | * browsing other people's feels from neighbor view 193 | * documentation browser""", 194 | "0.9.1": """ 195 | ver 0.9.1 features: 196 | * graffiti wall """, 197 | "0.9.2": """ 198 | ver 0.9.2 features: 199 | * paginated entry view 200 | * improved entry listing performance so it should 201 | be less sluggish (for now) 202 | * expanded menu for viewing your own feels (further features to be implemented) """, 203 | "0.9.3": """ 204 | version 0.9.3 features: 205 | * ttbp is now packaged, making it easier to contribute to. 206 | * things should otherwise be the same! 207 | * check out https://github.com/modgethanc/ttbp if you'd like to contribute. 208 | * takes advantage of new /var/global """, 209 | "0.10.1": """ 210 | ~[version 0.10.1 features]~ 211 | * thanks to help from ~vilmibm, ttbp now supports publishing to gopher! 212 | * if you enable gopher publishing, feels will automatically publish to 213 | gopher://tilde.town/1/~{user}/feels 214 | * if you don't know what gopher is, it's fine to opt-out; ask around on 215 | irc if you'd like to learn more! 216 | * the settings menu has been reworked to be less clunky""", 217 | "0.11.0": """ 218 | ~[version 0.11.0 update]~ 219 | 220 | * rainbow menus are now an option! please message ~endorphant (with 221 | screencaps, if possible) if rainbow menus are unreadable with your 222 | terminal settings, so adjustments can be made for future updates 223 | * you can now set individual posts to be published or not published; this 224 | option is listed under 'review your feels' as 'modify feels publishing', 225 | and is only available if you have publishing turned on. if you toggle 226 | your publishing state, this list will persist. unpublished posts will 227 | be removed from html/gopher, but will still be accessible from within 228 | the feels engine. 229 | * some errors in selecting and validating settings and creating publishing 230 | directories have been corrected 231 | * please send mail to ~endorphant or ask for help on IRC if you're still 232 | having issues with getting your settings sorted out! 233 | 234 | general PSA: 235 | * join #ttbp on the local irc network for help and discussion about the 236 | feels engine! 237 | * ~login created centralfeels, which is an opt-in collection of 238 | html-published feels; create a blank file called '.centralfeels' in 239 | your home directory if you'd like to be included!""", 240 | "0.11.1": """ 241 | ~[version 0.11.1 update]~ 242 | 243 | * a quick patch to correct a directory listing error, nothing too 244 | exciting 245 | * general PSA: feel free to use the github repo for bugs/feature requests: 246 | https://github.com/modgethanc/ttbp/issues""", 247 | "0.11.2": """ 248 | ~[version 0.11.2 update]~ 249 | 250 | * added a new option to allow setting entries to default to either public or 251 | non-public on posting; this option only really makes sense if you're 252 | already publishing to html/gopher, but is available either way! 253 | 254 | you can find this option under 'settings' as 'post as nopub'.""", 255 | "0.11.3": """ 256 | ~[version 0.11.3 update]~ 257 | 258 | * thanks to ~sinacutie, you can now set custom css for the permalink text 259 | styling on your html page. the default permalink style has been added to 260 | your current css file, and shouldn't change the appearance of your page. 261 | 262 | if you're not using custom css, don't worry about this!""", 263 | "0.12.0": """ 264 | ~[version 0.12.0 update]~ 265 | 266 | a lot of new stuff this time! from the main menu, option (1) is now called 267 | "manage your feels", which contains an expanded set of tools for organizing 268 | your feels: 269 | * read over feels (a list of all your entries) 270 | * modify feels publishing (toggle privacy on individual entries) 271 | * backup your feels (makes a .tar.gz of all your entries) 272 | * import a feels backup (unpacks a backup into your feels) 273 | * bury some feels (hide individual entries from view) 274 | * delete feels by day (permanently remove individual entries) 275 | * purge all feels (permanently remove all entries) 276 | * wipe feels account (permanently remove everything associated with 277 | ttbp) 278 | 279 | each of these tools has expanded descriptions and instructions from the 280 | menu, so check them out if you're curious! all of the irreversibl data 281 | management actions have confirmation actions, so it's easy to cancel if you 282 | accidentally access a tool. 283 | 284 | also, i've updated the documentation file to reflect recent feature changes, 285 | including some more details about how things work under the hood, and some 286 | clarifications on how existing features work. please give it a read through 287 | if you have a chance, and let me know if there's anything else i can 288 | improve! 289 | 290 | lastly, i just wanted to mention that i do accept tips for my dev work at 291 | https://liberapay.com/modgethanc/ but there's no pressure to donate at all, 292 | i'm just making this option available for anyone whose financially stable 293 | and wants to kick some spare change my way; this is a labor of love, and i'm 294 | happy to work on it regardless :) 295 | """, 296 | "0.12.1":""" 297 | ~[version 0.12.1 update]~ 298 | 299 | new feature: "visit your subscriptions" 300 | * view recent entries from a list of townies you've subscribed to 301 | * subscription list is private; no one else can see who you're following 302 | * add/remove users from the subscription menu 303 | 304 | minor changes: 305 | * global feed now shows 50 most recent entries, which you can scroll through 306 | * documentation page updated to reflect new feature 307 | * graffiti wall lock releases after 3 days 308 | 309 | thanks for those of your who've written me with feedback! 310 | 311 | keep feelin' together <3 312 | """, 313 | "0.12.2":""" 314 | ~[version 0.12.2 update]~ 315 | 316 | bug fix: ~epicmorphism helped out with fixing the pagination bug! now, when 317 | you scroll through a list of entries, it'll correctly return you to the 318 | page you were on if you open and close an item. thanks so much, friend <3 319 | 320 | thanks to everyone writing me with feedback; i've been real busy lately, 321 | and i might not get to making improvements or responding to mail for a 322 | while :( 323 | 324 | feel on! 325 | ~endo 326 | """ 327 | } 328 | -------------------------------------------------------------------------------- /ttbp/config/banner-beta.txt: -------------------------------------------------------------------------------- 1 | __________________________________________________________ 2 | | | 3 | | the tilde.town | 4 | | ____ ____ ____ _ ____ ____ _ _ ____ _ _ _ ____ | 5 | | |___ |___ |___ | [__ |___ |\ | | __ | |\ | |___ | 6 | | | |___ |___ |___ ___] |___ | \| |__] | | \| |___ | 7 | | | 8 | | ver 0.9.2 (almost stable) | 9 | |__________________________________________________________| 10 | -------------------------------------------------------------------------------- /ttbp/config/defaults/footer.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ttbp/config/defaults/header.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $USER on TTBP 5 | 6 | 7 | 8 |
9 |

~$USER@TTBP

10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /ttbp/config/defaults/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E0B0FF; 3 | font-family: courier 4 | } 5 | 6 | #meta { 7 | top: -.5em; 8 | position: fixed; 9 | height: 3.5em; 10 | float: left; 11 | text-align: left; 12 | width: 100%; 13 | background-color: #e0b0ff; 14 | } 15 | 16 | #tlogs { 17 | margin-top: 5em; 18 | width: 80%; 19 | } 20 | 21 | .entry { 22 | border: 1px dotted white; 23 | padding: .4em; 24 | margin-bottom:-4em; 25 | } 26 | 27 | .entry p { 28 | padding: 1em; 29 | } 30 | 31 | .entry p.permalink { 32 | font-size: .6em; 33 | font-color: #808080; 34 | text-align: right; 35 | } 36 | 37 | .entry h5 { 38 | text-align: right; 39 | margin-top: .2em; 40 | } 41 | 42 | blockquote { 43 | background-color: black; 44 | color: #e0b0ff; 45 | font-size: 90%; 46 | border: 1px dotted white; 47 | padding: 4px; 48 | } 49 | -------------------------------------------------------------------------------- /ttbp/core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | ttbp: tilde town blogging platform 5 | (also known as the feels engine) 6 | a console-based blogging program developed for tilde.town 7 | copyright (c) 2016 ~endorphant (endorphant@tilde.town) 8 | 9 | core.py: 10 | this is a core handler for some ttbp standalone/output functions 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | the complete codebase is available at: 32 | https://github.com/modgethanc/ttbp 33 | ''' 34 | 35 | import os 36 | import time 37 | import subprocess 38 | import re 39 | import mistune 40 | import json 41 | 42 | from . import chatter 43 | from . import config 44 | from . import gopher 45 | from . import util 46 | 47 | FEED = os.path.join("/home", "endorphant", "public_html", "ttbp", "index.html") 48 | SETTINGS = {} 49 | 50 | HEADER = "" 51 | FOOTER = "" 52 | FILES = [] 53 | NOPUBS = [] 54 | 55 | def load(ttbprc={}): 56 | ''' 57 | get all them globals set up!! 58 | ''' 59 | 60 | global HEADER 61 | global FOOTER 62 | global SETTINGS 63 | 64 | HEADER = open(os.path.join(config.USER_CONFIG, "header.txt")).read() 65 | FOOTER = open(os.path.join(config.USER_CONFIG, "footer.txt")).read() 66 | SETTINGS = ttbprc 67 | 68 | load_nopubs() 69 | load_files() 70 | 71 | def reload_ttbprc(ttbprc={}): 72 | ''' 73 | reloads new ttbprc into current session 74 | ''' 75 | 76 | global SETTINGS 77 | 78 | SETTINGS = ttbprc 79 | 80 | def get_files(feelsdir=config.MAIN_FEELS): 81 | """Returns a list of user's feels in the given directory (defaults to main 82 | feels dir)""" 83 | 84 | files = [] 85 | for filename in os.listdir(feelsdir): 86 | if nopub(filename): 87 | unpublish_feel(filename) 88 | else: 89 | filename = os.path.join(feelsdir, filename) 90 | if os.path.isfile(filename) and valid(filename): 91 | files.append(filename) 92 | 93 | files.sort() 94 | files.reverse() 95 | 96 | return files 97 | 98 | def load_files(feelsdir=config.MAIN_FEELS): 99 | ''' 100 | file loader 101 | 102 | * reads user's nopub file 103 | * calls get_files() to load all files for given directory 104 | * re-renders main html file and/or gopher if needed 105 | ''' 106 | 107 | global FILES 108 | 109 | load_nopubs() 110 | FILES = get_files(feelsdir) 111 | 112 | if publishing(): 113 | write_html("index.html") 114 | if SETTINGS.get('gopher'): 115 | gopher.publish_gopher('feels', FILES) 116 | 117 | def load_nopubs(): 118 | """Load a list of the user's nopub entries. 119 | """ 120 | 121 | global NOPUBS 122 | 123 | NOPUBS = [] 124 | 125 | if os.path.isfile(config.NOPUB): 126 | for line in open(config.NOPUB, "r"): 127 | if not re.match("^# ", line): 128 | NOPUBS.append(line.rstrip()) 129 | 130 | return len(NOPUBS) 131 | 132 | ## html outputting 133 | 134 | def write_html(outurl="default.html"): 135 | ''' 136 | main page renderer 137 | 138 | * takes everything currently in FILES and writes a single non-paginated html 139 | file 140 | * calls write_page() on each file to make permalinks 141 | ''' 142 | 143 | outfile = open(os.path.join(config.WWW, outurl), "w") 144 | 145 | outfile.write("\n\n") 146 | 147 | for line in HEADER: 148 | outfile.write(line) 149 | 150 | outfile.write("\n") 151 | 152 | for filename in FILES: 153 | write_page(filename) 154 | for line in write_entry(filename): 155 | outfile.write(line) 156 | 157 | outfile.write("\n") 158 | 159 | for line in FOOTER: 160 | outfile.write(line) 161 | 162 | outfile.close() 163 | 164 | return os.path.join(config.LIVE+config.USER,os.path.basename(os.path.realpath(config.WWW)),outurl) 165 | 166 | def write_page(filename): 167 | ''' 168 | permalink generator 169 | 170 | * makes a page out of a single entry for permalinking, using filename/date as 171 | url 172 | ''' 173 | 174 | outurl = os.path.join(config.WWW, "".join(util.parse_date(filename))+".html") 175 | outfile = open(outurl, "w") 176 | 177 | outfile.write("\n\n") 178 | 179 | for line in HEADER: 180 | outfile.write(line) 181 | 182 | outfile.write("\n") 183 | 184 | for line in write_entry(filename): 185 | outfile.write(line) 186 | 187 | outfile.write("\n") 188 | 189 | for line in FOOTER: 190 | outfile.write(line) 191 | 192 | outfile.close() 193 | 194 | return outurl 195 | 196 | def write_entry(filename): 197 | ''' 198 | entry text generator 199 | 200 | * dump given file into entry format by parsing file as markdown 201 | * return as list of strings 202 | ''' 203 | 204 | date = util.parse_date(filename) 205 | 206 | entry = [ 207 | "\t\t



\n", 208 | "\t\t
\n", 209 | "\t\t\t
"+date[2]+" "+chatter.month(date[1])+" "+date[0]+"
\n" 210 | #"\t\t\t

" 211 | ] 212 | 213 | raw = [] 214 | rawfile = open(os.path.join(config.MAIN_FEELS, filename), "r") 215 | 216 | for line in rawfile: 217 | raw.append(line) 218 | rawfile.close() 219 | 220 | entry.append("\t\t\t"+mistune.markdown("".join(raw), escape=False, hard_wrap=False)) 221 | 222 | #for line in raw: 223 | #entry.append(line+"\t\t\t") 224 | #if line == "\n": 225 | # entry.append("

\n\t\t\t

") 226 | 227 | #entry.append("

\n") 228 | entry.append("\t\t\t

permalink

\n") 229 | entry.append("\n\t\t
\n") 230 | 231 | return entry 232 | 233 | def write_global_feed(blogList): 234 | ''' 235 | main ttbp index printer 236 | 237 | * sources README.md for documentation 238 | * takes incoming list of formatted blog links for all publishing blogs and 239 | prints to blog feed 240 | ''' 241 | 242 | try: 243 | outfile = open(FEED, "w") 244 | 245 | ## header 246 | outfile.write("""\ 247 | 248 | 249 | 250 | tilde.town feels engine 251 | 252 | 253 | 254 |
255 |

tilde.town feels engine

256 | 257 |

github 258 | repo | state 260 | of the ttbp

261 |
263 |

 

264 | """) 265 | 266 | ## docs 267 | outfile.write("""\ 268 |
""") 269 | outfile.write(mistune.markdown(open(os.path.join(config.INSTALL_PATH, "..", "README.md"), "r").read())) 270 | outfile.write("""\ 271 |
""") 272 | 273 | ## feed 274 | outfile.write("""\ 275 |

 

276 |
277 |

live feels-sharing:

278 |
    """) 279 | for blog in blogList: 280 | outfile.write(""" 281 |
  • """+blog+"""
  • \ 282 | """) 283 | 284 | ## footer 285 | outfile.write(""" 286 |
287 |
288 | 289 | 290 | """) 291 | 292 | outfile.close() 293 | #subprocess.call(['chmod', 'a+w', FEED]) 294 | except FileNotFoundError: 295 | pass 296 | 297 | ## misc helpers 298 | 299 | def meta(entries = FILES): 300 | ''' 301 | metadata generator 302 | 303 | * takes a list of filenames and returns a 2d list: 304 | [0] absolute path 305 | [1] mtime 306 | [2] wc -w 307 | [3] timestamp "DD month YYYY at HH:MM" 308 | [4] entry date YYYY-MM-DD 309 | [5] author 310 | 311 | * sorted in reverse date order by [4] 312 | ''' 313 | 314 | meta = [] 315 | 316 | for filename in entries: 317 | mtime = os.path.getmtime(filename) 318 | try: 319 | wc = int(subprocess.check_output(["wc","-w",filename], stderr=subprocess.STDOUT).split()[0]) 320 | except subprocess.CalledProcessError: 321 | wc = "???" 322 | timestamp = time.strftime("%Y-%m-%d at %H:%M", time.localtime(mtime)) 323 | date = "-".join(util.parse_date(filename)) 324 | author = os.path.split(os.path.split(os.path.split(os.path.split(filename)[0])[0])[0])[1] 325 | 326 | meta.append([filename, mtime, wc, timestamp, date, author]) 327 | 328 | #meta.sort(key = lambda filename:filename[4]) 329 | #meta.reverse() 330 | 331 | return meta 332 | 333 | def valid(filename): 334 | ''' 335 | filename validator 336 | 337 | * check if the filename is YYYYMMDD.txt 338 | ''' 339 | 340 | filesplit = os.path.splitext(os.path.basename(filename)) 341 | 342 | if filesplit[1] != ".txt": 343 | return False 344 | 345 | pattern = '^((19|20)\d{2})(0[1-9]|1[0-2])(0[1-9]|1\d|2\d|3[01])$' 346 | 347 | if not re.match(pattern, filesplit[0]): 348 | return False 349 | 350 | return True 351 | 352 | def find_ttbps(): 353 | ''' 354 | returns a list of users with a ttbp by checking for a valid ttbprc 355 | ''' 356 | 357 | users = [] 358 | 359 | for townie in os.listdir("/home"): 360 | if os.path.exists(os.path.join("/home", townie, ".ttbp", "config", "ttbprc")): 361 | users.append(townie) 362 | 363 | return users 364 | 365 | def publishing(username=config.USER): 366 | ''' 367 | checks .ttbprc for whether or not user opted for www publishing 368 | ''' 369 | 370 | ttbprc = {} 371 | 372 | if username == config.USER: 373 | ttbprc = SETTINGS 374 | 375 | else: 376 | ttbprc = json.load(open(os.path.join("/home", username, ".ttbp", "config", "ttbprc"))) 377 | 378 | return ttbprc.get("publishing") 379 | 380 | def www_neighbors(): 381 | ''' 382 | takes a list of users with publiishing turned on and prepares it for www output 383 | ''' 384 | 385 | userList = [] 386 | 387 | for user in find_ttbps(): 388 | if not publishing(user): 389 | continue 390 | 391 | userRC = json.load(open(os.path.join("/home", user, ".ttbp", "config", "ttbprc"))) 392 | 393 | url = "" 394 | if userRC["publish dir"]: 395 | url = config.LIVE+user+"/"+userRC["publish dir"] 396 | 397 | lastfile = "" 398 | try: 399 | files = os.listdir(os.path.join("/home", user, ".ttbp", "entries")) 400 | except OSError: 401 | files = [] 402 | files.sort() 403 | for filename in files: 404 | if valid(filename): 405 | lastfile = os.path.join("/home", user, ".ttbp", "entries", filename) 406 | 407 | if lastfile: 408 | last = os.path.getctime(lastfile) 409 | timestamp = time.strftime("%Y-%m-%d at %H:%M", time.localtime(last)) + " (utc"+time.strftime("%z")[0]+time.strftime("%z")[2]+")" 410 | else: 411 | timestamp = "" 412 | last = 0 413 | 414 | userList.append(["~"+user+" "+timestamp, last]) 415 | 416 | # sort user by most recent entry 417 | userList.sort(key = lambda userdata:userdata[1]) 418 | userList.reverse() 419 | sortedUsers = [] 420 | for user in userList: 421 | sortedUsers.append(user[0]) 422 | 423 | write_global_feed(sortedUsers) 424 | 425 | def nopub(filename): 426 | ''' 427 | checks to see if given filename is in user's NOPUB 428 | ''' 429 | 430 | return os.path.basename(filename) in NOPUBS 431 | 432 | def toggle_nopub(filename): 433 | """toggles pub/nopub status for the given filename 434 | 435 | if the file is to be unpublished, delete it from published locations 436 | """ 437 | 438 | global NOPUBS 439 | 440 | action = "unpublishing" 441 | 442 | if nopub(filename): 443 | action = "publishing" 444 | NOPUBS.remove(filename) 445 | else: 446 | NOPUBS.append(filename) 447 | unpublish_feel(filename) 448 | 449 | nopub_file = open(config.NOPUB, 'w') 450 | nopub_file.write("""\ 451 | # files that don't get published html/gopher. this file is 452 | # generated by ttbp; editing it directly may result in unexpected 453 | # behavior. if you have problems, back up this file, delete it, and 454 | # rebuild it from ttbp.\n""") 455 | for entry in NOPUBS: 456 | nopub_file.write(entry+"\n") 457 | nopub_file.close() 458 | 459 | load_files() 460 | 461 | return action 462 | 463 | def bury_feel(filename): 464 | """buries given filename; this removes the feel from any publicly-readable 465 | location, and moves the textfile to user's private feels directory. 466 | 467 | timestring will be added to the filename to disambiguate and prevent 468 | filename collisions. 469 | 470 | creates buried feels dir if it doesn't exist. 471 | 472 | regenerates feels list and republishes.""" 473 | 474 | if not os.path.exists(config.BURIED_FEELS): 475 | os.mkdir(config.BURIED_FEELS) 476 | subprocess.call(["chmod", "700", config.BURIED_FEELS]) 477 | 478 | buryname = os.path.splitext(os.path.basename(filename))[0]+"-"+str(int(time.time()))+".txt" 479 | 480 | subprocess.call(["mv", os.path.join(config.MAIN_FEELS, filename), os.path.join(config.BURIED_FEELS, buryname)]) 481 | subprocess.call(["chmod", "600", os.path.join(config.BURIED_FEELS, buryname)]) 482 | 483 | if publishing(): 484 | unpublish_feel(filename) 485 | 486 | load_files() 487 | 488 | return os.path.join(config.BURIED_FEELS, buryname) 489 | 490 | def delete_feel(filename): 491 | """deletes given filename; removes the feel from publicly-readable 492 | locations, then deletes the original file.""" 493 | 494 | feel = os.path.join(config.MAIN_FEELS, filename) 495 | if os.path.exists(feel): 496 | subprocess.call(["rm", feel]) 497 | unpublish_feel(filename) 498 | load_files(config.MAIN_FEELS) 499 | 500 | def unpublish_feel(filename): 501 | """takes given filename and removes it from public_html and gopher_html, if 502 | those locations exists. afterwards, regenerate index files appropriately.""" 503 | 504 | live_html = os.path.join(config.WWW, 505 | os.path.splitext(os.path.basename(filename))[0]+".html") 506 | if os.path.exists(live_html): 507 | subprocess.call(["rm", live_html]) 508 | live_gopher = os.path.join(config.GOPHER_PATH, filename) 509 | if os.path.exists(live_gopher): 510 | subprocess.call(["rm", live_gopher]) 511 | 512 | def process_backup(filename): 513 | """takes given filename and unpacks it into a temp directory, then returns a 514 | list of filenames with collisions filtered out. 515 | 516 | ignores any invalidly named files or files that already exist, to avoid 517 | clobbering current feels. ignored files are left in the archive directory 518 | for the user to manually sort out.""" 519 | 520 | backup_dir = os.path.splitext(os.path.splitext(os.path.basename(filename))[0])[0] 521 | backup_path = os.path.join(config.BACKUPS, backup_dir) 522 | 523 | if not os.path.exists(backup_path): 524 | subprocess.call(["mkdir", backup_path]) 525 | 526 | subprocess.call(["chmod", "700", backup_path]) 527 | subprocess.call(["tar", "-C", backup_path, "-xf", filename]) 528 | backup_entries = os.path.join(backup_path, "entries") 529 | 530 | backups = os.listdir(backup_entries) 531 | current = os.listdir(config.MAIN_FEELS) 532 | 533 | imported = [] 534 | 535 | for feel in backups: 536 | if os.path.basename(feel) not in current: 537 | imported.append(os.path.join(backup_entries, feel)) 538 | 539 | imported.sort() 540 | return imported 541 | 542 | def import_feels(backups): 543 | """takes a list of filepaths and copies those to current main feels. 544 | 545 | this does not check for collisions. 546 | """ 547 | 548 | pass 549 | 550 | 551 | ############# 552 | ############# 553 | ############# 554 | 555 | def test(): 556 | load() 557 | 558 | metaTest = meta() 559 | 560 | for x in metaTest: 561 | print(x) 562 | -------------------------------------------------------------------------------- /ttbp/gopher.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains gopher-related stuff. 3 | """ 4 | import getpass 5 | import os 6 | import time 7 | import subprocess 8 | 9 | from . import util 10 | from . import config 11 | #from .core import parse_date 12 | 13 | GOPHER_PROMPT = """ 14 | 15 | GOPHER SETUP 16 | 17 | gopher is a pre-web technology that is text-oriented and primarily used to 18 | share folders of your files with the world. 19 | 20 | Would you like to publish your feels to gopher? 21 | 22 | Entries you write will automatically get linked to ~/public_gopher/feels/, 23 | including gophermap generation. Files you manually delete will no longer be 24 | visible from your gopherhole, and will be purged from your gophermap on your 25 | next entry update. 26 | 27 | If you don't know what it is or don't want it that is totally ok! 28 | 29 | You can always change this later.""".lstrip() 30 | 31 | GOPHERMAP_HEADER = """ 32 | welcome to {user}'s gopherfeels. 33 | 34 | .:: .:: 35 | .: .:: 36 | .:.: .: .:: .:: .:: .:::: 37 | .:: .: .:: .: .:: .::.:: 38 | .:: .::::: .::.::::: .:: .:: .::: 39 | .:: .: .: .:: .:: 40 | .:: .:::: .:::: .:::.:: .:: 41 | 42 | this file is automatically generated by ttbp. 43 | 44 | 0(about ttbp)\t/~endorphant/ttbp.txt\ttilde.town\t70 45 | 1(back to user's home)\t/~{user} 46 | 47 | entries: 48 | 49 | """ 50 | 51 | 52 | def select_gopher(): 53 | return util.input_yn(GOPHER_PROMPT) 54 | 55 | def publish_gopher(gopher_path, entry_filenames): 56 | """This function (re)generates a user's list of feels posts in their gopher 57 | directory and their gophermap.""" 58 | entry_filenames = entry_filenames[:] # force a copy since this might be shared state in core.py 59 | ttbp_gopher = os.path.join( 60 | os.path.expanduser('~/public_gopher'), 61 | gopher_path) 62 | 63 | if not os.path.isdir(ttbp_gopher): 64 | print('\n\tERROR: something is wrong. your gopher directory is missing. re-enable gopher publishing from the settings menu to fix this up!') 65 | return 66 | 67 | with open(os.path.join(ttbp_gopher, 'gophermap'), 'w') as gophermap: 68 | gophermap.write(GOPHERMAP_HEADER.format( 69 | user=getpass.getuser())) 70 | for entry_filename in entry_filenames: 71 | filename = os.path.basename(entry_filename) 72 | 73 | gopher_entry_symlink = os.path.join(ttbp_gopher, os.path.basename(entry_filename)) 74 | if not os.path.exists(gopher_entry_symlink): 75 | subprocess.call(["ln", "-s", entry_filename, gopher_entry_symlink]) 76 | 77 | label = "-".join(util.parse_date(entry_filename)) 78 | gophermap.write('0{file_label}\t{filename}\n'.format( 79 | file_label=label, 80 | filename=filename)) 81 | 82 | def setup_gopher(gopher_path): 83 | """Given a path relative to ~/public_gopher, this function: 84 | 85 | - creates a directory ~/.ttbp/gopher 86 | - symlinks that directory to ~/public_gopher/{gopher_path} 87 | 88 | It doesn't create a gophermap as that is left to the publish_gopher 89 | function. 90 | """ 91 | public_gopher = os.path.expanduser('~/public_gopher') 92 | 93 | if not os.path.isdir(public_gopher): 94 | """ 95 | print("\n\tERROR: you don't seem to have gopher set up (no public_gopher directory)") 96 | return 97 | """ 98 | os.makedirs(public_gopher) 99 | 100 | ttbp_gopher = os.path.join(public_gopher, gopher_path) 101 | 102 | if os.path.isdir(ttbp_gopher): 103 | print("\n\tERROR: gopher path is already set up. quitting so we don't overwrite anything.") 104 | return 105 | 106 | gopher_entries = os.path.join(os.path.expanduser("~/.ttbp"), "gopher") 107 | if not os.path.isdir(gopher_entries): 108 | os.makedirs(gopher_entries) 109 | 110 | subprocess.call(["ln", "-s", gopher_entries, ttbp_gopher]) 111 | 112 | def unpublish(): 113 | """blanks all gopher things and recreates the directories.""" 114 | 115 | subprocess.call(["rm", "-rf", config.GOPHER_PATH]) 116 | subprocess.call(["rm", "-rf", config.GOPHER_ENTRIES]) 117 | os.mkdir(config.GOPHER_ENTRIES) 118 | subprocess.call(["ln", "-s", config.GOPHER_ENTRIES, config.GOPHER_PATH]) 119 | -------------------------------------------------------------------------------- /ttbp/ttbp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | ttbp: tilde town blogging platform 5 | (also known as the feels engine) 6 | a console-based blogging program developed for tilde.town 7 | copyright (c) 2016 ~endorphant (endorphant@tilde.town) 8 | 9 | ttbp.py: 10 | the main console interface 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | the complete codebase is available at: 32 | https://github.com/modgethanc/ttbp 33 | ''' 34 | from __future__ import absolute_import 35 | 36 | import os 37 | import sys 38 | import tempfile 39 | import subprocess 40 | import time 41 | import json 42 | from email.mime.text import MIMEText 43 | import datetime 44 | from six.moves import input 45 | 46 | import inflect 47 | 48 | from . import config 49 | from . import core 50 | from . import chatter 51 | from . import gopher 52 | from . import util 53 | 54 | __version__ = "0.12.2" 55 | __author__ = "endorphant {leftover}\n".format(leftover=leftover)) 174 | 175 | def main(): 176 | ''' 177 | main engine head 178 | 179 | * called on program start 180 | * calls config check 181 | * proceeds to main menu 182 | * handles ^c and ^d ejects 183 | ''' 184 | 185 | redraw() 186 | print(""" 187 | if you don't want to be here at any point, press and it'll all go away. 188 | just keep in mind that you might lose anything you've started here.\ 189 | """) 190 | 191 | try: 192 | print(check_init()) 193 | except EOFError: 194 | print(stop()) 195 | return 196 | 197 | redraw() 198 | 199 | while 1: 200 | try: 201 | print(main_menu()) 202 | except EOFError: 203 | print(stop()) 204 | break 205 | except KeyboardInterrupt: 206 | redraw(EJECT) 207 | else: 208 | break 209 | 210 | def stop(): 211 | ''' 212 | returns an exit message. 213 | ''' 214 | 215 | return "\n\n\t"+chatter.say("bye")+"\n\n" 216 | 217 | def check_init(): 218 | ''' 219 | user environment validation 220 | 221 | * checks for presence of ttbprc 222 | * checks for last run version 223 | ''' 224 | 225 | print("\n\n") 226 | if os.path.exists(os.path.join(os.path.expanduser("~"),".ttbp")): 227 | if config.USER == "endorphant": 228 | print("hey boss! :D\n") 229 | else: 230 | print("{greeting}, {user}".format(greeting=chatter.say("greet"), 231 | user=config.USER)) 232 | 233 | load_settings = load_user_settings() 234 | 235 | ## ttbp env validation 236 | if not user_up_to_date(): 237 | update_user_version() 238 | 239 | if not valid_setup(load_settings): 240 | setup_repair() 241 | else: 242 | input("press to explore your feels.\n\n") 243 | 244 | core.load(SETTINGS) 245 | 246 | return "" 247 | else: 248 | return init() 249 | 250 | def init(): 251 | """Initializes new user by setting up ~/.ttbp directory and config file. 252 | """ 253 | 254 | try: 255 | input(config.intro_prompt) 256 | except KeyboardInterrupt: 257 | print("\n\nthanks for checking in! i'll always be here.\n\n") 258 | quit() 259 | 260 | print("\nokay! gimme a second to get you set up!") 261 | 262 | time.sleep(1) 263 | print("...") 264 | time.sleep(.5) 265 | 266 | ## record user in source list 267 | users = open(config.USERFILE, 'a') 268 | users.write(config.USER+"\n") 269 | users.close() 270 | 271 | #subprocess.call(['chmod', 'a+w', config.USERFILE]) 272 | 273 | ## make .ttbp directory structure 274 | print("\ngenerating feels at {path}...".format(path=config.PATH).rstrip()) 275 | subprocess.call(["mkdir", config.PATH]) 276 | subprocess.call(["mkdir", config.USER_CONFIG]) 277 | subprocess.call(["mkdir", config.MAIN_FEELS]) 278 | 279 | versionFile = os.path.join(config.PATH, "version") 280 | open(versionFile, "w").write(__version__) 281 | 282 | ## create header file 283 | header = gen_header() 284 | headerfile = open(os.path.join(config.USER_CONFIG, "header.txt"), 'w') 285 | for line in header: 286 | headerfile.write(line) 287 | headerfile.close() 288 | 289 | ## copy footer and default stylesheet 290 | with open(os.path.join(config.USER_CONFIG, 'footer.txt'), 'w') as f: 291 | f.write(config.DEFAULT_FOOTER) 292 | with open(os.path.join(config.USER_CONFIG, 'style.css'), 'w') as f: 293 | f.write(config.DEFAULT_STYLE) 294 | 295 | ## run user-interactive setup and load core engine 296 | time.sleep(0.5) 297 | print("done setting up feels!") 298 | print("\nthese are the default settings. you can change any of them now, or change them later at any time!!") 299 | setup() 300 | core.load(SETTINGS) 301 | 302 | input(""" 303 | 304 | you're all good to go, {friend}! if you have any questions about how things 305 | work here, check out the documentation from the main menu, ask in IRC, or 306 | drop ~endorphant a line! 307 | 308 | hit to continue. 309 | """.format(friend=chatter.say("friend"))) 310 | 311 | return "" 312 | 313 | def gen_header(): 314 | ''' 315 | header generator 316 | 317 | builds header to insert username 318 | ''' 319 | 320 | header =""" 321 | 322 | 323 | 324 | 325 | ~"""+config.USER+""" on TTBP 326 | 327 | 328 | 329 | 332 | 333 | 334 | 335 | 336 | 337 | 338 |
\ 339 | """ 340 | return header 341 | 342 | def valid_setup(load_settings): 343 | ''' 344 | Checks to see if user has a valid ttbp environment. 345 | ''' 346 | 347 | if not load_settings: 348 | return False 349 | 350 | for option in iter(DEFAULT_SETTINGS): 351 | if option != "publish dir" and SETTINGS.get(option, None) is None: 352 | return False 353 | 354 | if core.publishing(): 355 | if SETTINGS.get("publish dir", None) is None: 356 | print("CONFIG ERROR! publishing is enabled but no directory is set") 357 | return False 358 | 359 | if (not os.path.exists(config.WWW) or 360 | not os.path.exists(os.path.join(config.PUBLIC, 361 | SETTINGS.get("publish dir")))): 362 | print("something's weird with your publishing directories. let's try rebuilding them!") 363 | 364 | update_publishing() 365 | 366 | return True 367 | 368 | def load_user_settings(): 369 | """attempts to load user's ttbprc; returns settings dict if valie, otherwise 370 | returns false""" 371 | 372 | global SETTINGS 373 | 374 | if not os.path.isfile(config.TTBPRC): 375 | return False 376 | 377 | try: 378 | SETTINGS = json.load(open(config.TTBPRC)) 379 | except ValueError: 380 | return False 381 | 382 | core.load(SETTINGS) 383 | 384 | return SETTINGS 385 | 386 | def setup_repair(): 387 | ''' 388 | setup repair function 389 | 390 | * calls setup() 391 | * handles ^c 392 | ''' 393 | 394 | global SETTINGS 395 | 396 | print("\nyour ttbp configuration doesn't look right. let me try to fix it....\n\n") 397 | 398 | time.sleep(1) 399 | 400 | settings_map = { 401 | "editor": select_editor, 402 | "publishing": select_publishing, 403 | "publish dir": select_publish_dir, 404 | "gopher": gopher.select_gopher, 405 | "rainbows": toggle_rainbows, 406 | "post as nopub": toggle_pub_default 407 | } 408 | 409 | for option in iter(settings_map): 410 | if SETTINGS.get(option, None) is None: 411 | SETTINGS.update({option: "NOT SET"}) 412 | SETTINGS.update({option: settings_map[option]()}) 413 | 414 | update_publishing() 415 | core.reload_ttbprc(SETTINGS) 416 | save_settings() 417 | 418 | print("...") 419 | time.sleep(0.5) 420 | input("\nyou're all good to go, "+chatter.say("friend")+"! hit to continue.\n\n") 421 | 422 | def setup(): 423 | ''' 424 | master setup function 425 | 426 | * editor selection 427 | * publishing toggle (publish/unpublish as needed) 428 | * directory selection 429 | * gopher opt in/out 430 | 431 | TODO: break this out better? 432 | ''' 433 | 434 | global SETTINGS 435 | 436 | menuOptions = [] 437 | settingList = sorted(list(SETTINGS)) 438 | 439 | for setting in settingList: 440 | menuOptions.append(setting + ": \t" + str(SETTINGS.get(setting))) 441 | util.print_menu(menuOptions, SETTINGS.get("rainbows", False)) 442 | 443 | try: 444 | choice = input("\npick a setting to change (or type 'q' to exit): ") 445 | except KeyboardInterrupt: 446 | redraw(EJECT) 447 | return SETTINGS 448 | 449 | if choice is not "": 450 | 451 | if choice in QUITS: 452 | redraw() 453 | return SETTINGS 454 | 455 | # editor selection 456 | if settingList[int(choice)] == "editor": 457 | SETTINGS.update({"editor": select_editor()}) 458 | redraw("text editor set to: {editor}".format(editor=SETTINGS["editor"])) 459 | save_settings() 460 | return setup() 461 | 462 | # publishing selection 463 | elif settingList[int(choice)] == "publishing": 464 | SETTINGS.update({"publishing":select_publishing()}) 465 | core.reload_ttbprc(SETTINGS) 466 | update_publishing() 467 | redraw("publishing set to {publishing}".format(publishing=SETTINGS.get("publishing"))) 468 | save_settings() 469 | return setup() 470 | 471 | # publish dir selection 472 | elif settingList[int(choice)] == "publish dir": 473 | publish_dir = select_publish_dir() 474 | SETTINGS.update({"publish dir": publish_dir}) 475 | #update_publishing() 476 | 477 | if publish_dir is None: 478 | redraw("sorry, i can't set a publish directory for you if you don't have html publishing enabled. please enable publishing to continue.") 479 | else: 480 | redraw("publishing your entries to {url}/index.html".format( 481 | url="/".join([config.LIVE+config.USER, 482 | str(SETTINGS.get("publish dir"))]))) 483 | save_settings() 484 | return setup() 485 | 486 | # gopher opt-in 487 | elif settingList[int(choice)] == "gopher": 488 | SETTINGS.update({'gopher': gopher.select_gopher()}) 489 | redraw('gopher publishing set to: {gopher}'.format(gopher=SETTINGS['gopher'])) 490 | update_gopher() 491 | save_settings() 492 | return setup() 493 | 494 | # rainbow menu selection 495 | elif settingList[int(choice)] == "rainbows": 496 | SETTINGS.update({"rainbows": toggle_rainbows()}) 497 | redraw("rainbow menus set to {rainbow}".format(rainbow=SETTINGS.get("rainbows"))) 498 | save_settings() 499 | return setup() 500 | 501 | #nopub toggling 502 | elif settingList[int(choice)] == "post as nopub": 503 | SETTINGS.update({"post as nopub": toggle_pub_default()}) 504 | redraw("posting default set to {nopub}".format(nopub=SETTINGS.get("post as nopub"))) 505 | save_settings() 506 | return setup() 507 | 508 | input("\nyou're all good to go, {friend}! hit to continue.\n\n".format(friend=chatter.say("friend"))) 509 | redraw() 510 | 511 | return SETTINGS 512 | 513 | else: 514 | redraw("now changing your settings. press if you didn't mean to do this.") 515 | return setup() 516 | 517 | def save_settings(): 518 | """ 519 | Save current settings. 520 | """ 521 | 522 | ttbprc = open(config.TTBPRC, "w") 523 | ttbprc.write(json.dumps(SETTINGS, sort_keys=True, indent=2, separators=(',',':'))) 524 | ttbprc.close() 525 | 526 | ## menus 527 | 528 | def main_menu(): 529 | ''' 530 | main navigation menu 531 | ''' 532 | 533 | menuOptions = [ 534 | "record some feels", 535 | "manage your feels", 536 | "check out your neighbors", 537 | "browse global feels", 538 | "visit your subscriptions", 539 | "scribble some graffiti", 540 | "change your settings", 541 | "send some feedback", 542 | "see credits", 543 | "read documentation"] 544 | 545 | print("you're at ttbp home. remember, you can always press to come back here.\n") 546 | util.print_menu(menuOptions, SETTINGS.get("rainbows", False)) 547 | 548 | try: 549 | choice = input("\ntell me about your feels (or type 'q' to exit): ") 550 | except KeyboardInterrupt: 551 | redraw(EJECT) 552 | return main_menu() 553 | 554 | if choice == '0': 555 | redraw() 556 | today = time.strftime("%Y%m%d") 557 | write_entry(os.path.join(config.MAIN_FEELS, today+".txt")) 558 | core.www_neighbors() 559 | elif choice == '1': 560 | intro = "here are some options for managing your feels:" 561 | redraw(intro) 562 | review_menu(intro) 563 | core.load_files() 564 | elif choice == '2': 565 | users = core.find_ttbps() 566 | prompt = "the following {usercount} {are} recording feels on ttbp:".format( 567 | usercount=p.no("user", len(users)), 568 | are=p.plural("is", len(users))) 569 | redraw(prompt) 570 | view_neighbors(users, prompt) 571 | elif choice == '3': 572 | redraw("most recent global entries") 573 | view_global_feed() 574 | elif choice == '4': 575 | intro = "your subscriptions list is private; no one but you will know who you're following.\n\n> here are some options for your subscriptions:" 576 | redraw(intro) 577 | subscription_handler(intro) 578 | elif choice == '5': 579 | graffiti_handler() 580 | elif choice == '6': 581 | redraw("now changing your settings. press if you didn't mean to do this.") 582 | core.load(setup()) # reload settings to core 583 | elif choice == '7': 584 | redraw("you're about to send mail to ~endorphant about ttbp") 585 | feedback_menu() 586 | elif choice == '8': 587 | redraw() 588 | show_credits() 589 | elif choice == '9': 590 | subprocess.call(["lynx", os.path.join(config.INSTALL_PATH, "..", "doc", "manual.html")]) 591 | redraw() 592 | elif choice in QUITS: 593 | return stop() 594 | else: 595 | redraw(INVALID) 596 | 597 | return main_menu() 598 | 599 | def feedback_menu(): 600 | ''' 601 | feedback handling menu 602 | 603 | * selects feedback type 604 | * calls feedback writing function 605 | ''' 606 | 607 | util.print_menu(SUBJECTS, SETTINGS.get("rainbows", False)) 608 | choice = input("\npick a category for your feedback: ") 609 | 610 | cat = "" 611 | if choice in ['0', '1', '2', '3']: 612 | cat = SUBJECTS[int(choice)] 613 | entered = input(""" 614 | composing a {mail_category} to ~endorphant. 615 | 616 | press to open an external text editor. mail will be sent once you save and quit. 617 | 618 | """.format(mail_category=cat)) 619 | redraw(send_feedback(entered, cat)) 620 | return 621 | else: 622 | redraw(INVALID) 623 | 624 | return feedback_menu() 625 | 626 | def review_menu(intro=""): 627 | ''' 628 | submenu for reviewing feels. 629 | ''' 630 | 631 | menuOptions = [ 632 | "read over feels", 633 | "modify feels publishing", 634 | "backup your feels", 635 | "import a feels backup", 636 | "bury some feels", 637 | "delete feels by day", 638 | "purge all feels", 639 | "wipe feels account" 640 | ] 641 | 642 | util.print_menu(menuOptions, SETTINGS.get("rainbows", False)) 643 | 644 | choice = util.list_select(menuOptions, "what would you like to do with your feels? (or 'q' to return home) ") 645 | 646 | top = "" 647 | hasfeels = len(os.listdir(config.MAIN_FEELS)) > 0 648 | nofeels = "you don't have any feels to work with, "+chatter.say("friend")+"\n\n> " 649 | 650 | if choice is not False: 651 | if choice == 0: 652 | if hasfeels: 653 | redraw("your recorded feels, listed by date:") 654 | view_feels(config.USER) 655 | else: 656 | top = nofeels 657 | elif choice == 1: 658 | if hasfeels: 659 | redraw("publishing status of your feels:") 660 | list_nopubs(config.USER) 661 | else: 662 | top = nofeels 663 | elif choice == 2: 664 | if hasfeels: 665 | redraw("FEELS BACKUP") 666 | backup_feels() 667 | else: 668 | top = nofeels 669 | elif choice == 3: 670 | redraw("loading feels backup") 671 | load_backup() 672 | elif choice == 4: 673 | if hasfeels: 674 | redraw("burying feels") 675 | bury_feels() 676 | else: 677 | top = nofeels 678 | elif choice == 5: 679 | if hasfeels: 680 | redraw("deleting feels") 681 | delete_feels() 682 | else: 683 | top = nofeels 684 | elif choice == 6: 685 | if hasfeels: 686 | redraw("!!!PURGING ALL FEELS!!!") 687 | purge_feels() 688 | else: 689 | top = nofeels 690 | elif choice == 7: 691 | redraw("!!! WIPING FEELS ACCOUNT !!!") 692 | wipe_account() 693 | else: 694 | redraw() 695 | return 696 | 697 | redraw(top+intro) 698 | return review_menu(intro) 699 | 700 | def subscription_handler(intro=""): 701 | ''' 702 | submenu for managing subscriptions 703 | ''' 704 | 705 | if not os.path.exists(config.SUBS): 706 | subprocess.call(["touch", config.SUBS]) 707 | subprocess.call(["chmod", "600", config.SUBS]) 708 | 709 | subs_raw = [] 710 | if os.path.isfile(config.SUBS): 711 | for line in open(config.SUBS, "r"): 712 | subs_raw.append(line.rstrip()) 713 | 714 | subs = [] 715 | all_users = core.find_ttbps() 716 | for name in subs_raw: 717 | if name in all_users: 718 | subs.append(name) 719 | 720 | menuOptions = [ 721 | "view subscribed feed", 722 | "manage subscriptions" 723 | ] 724 | 725 | util.print_menu(menuOptions, SETTINGS.get("rainbows", False)) 726 | 727 | choice = util.list_select(menuOptions, "what would you like to do with your subscriptions? (or 'q' to return home) ") 728 | 729 | top = "" 730 | 731 | if choice is not False: 732 | if choice == 0: 733 | if len(subs) > 0: 734 | prompt = "most recent entries from your subscribed pals:" 735 | redraw(prompt) 736 | view_subscribed_feed(subs, prompt) 737 | else: 738 | intro = "it doesn't look like you have any subscriptions to see! add pals with 'manage subscriptions' here." 739 | elif choice == 1: 740 | prompt = "options for managing your subscriptions:" 741 | redraw(prompt) 742 | subscription_manager(subs, prompt) 743 | else: 744 | redraw() 745 | return 746 | 747 | redraw(top+intro) 748 | return subscription_handler(intro) 749 | 750 | def view_neighbors(users, prompt, page=0): 751 | ''' 752 | generates list of all users on ttbp, sorted by most recent post 753 | 754 | * if user is publishing, list publish directory 755 | ''' 756 | 757 | userList = [] 758 | 759 | ## assumes list of users passed in all have valid config files 760 | for user in users: 761 | userRC = json.load(open(os.path.join("/home", user, ".ttbp", "config", "ttbprc"))) 762 | 763 | ## retrieve publishing url, if it exists 764 | url="\t\t\t" 765 | if userRC.get("publish dir"): 766 | url = config.LIVE+user+"/"+userRC.get("publish dir") 767 | 768 | ## find last entry 769 | try: 770 | files = os.listdir(os.path.join("/home", user, ".ttbp", "entries")) 771 | except OSError: 772 | files = [] 773 | files.sort() 774 | lastfile = "" 775 | for filename in files: 776 | if core.valid(filename): 777 | lastfile = os.path.join("/home", user, ".ttbp", "entries", filename) 778 | 779 | ## generate human-friendly timestamp 780 | ago = "never" 781 | if lastfile: 782 | last = os.path.getctime(lastfile) 783 | since = time.time()-last 784 | ago = util.pretty_time(int(since)) + " ago" 785 | else: 786 | last = 0 787 | 788 | ## some formatting handwavin 789 | urlpad = "" 790 | if ago == "never": 791 | urlpad = "\t" 792 | 793 | userpad = "" 794 | if len(user) < 7: 795 | userpad = "\t" 796 | 797 | userList.append(["\t~{user}{userpad}\t({ago}){urlpad}\t{url}".format(user=user, 798 | userpad=userpad, ago=ago, urlpad=urlpad, url=url), last, user]) 799 | 800 | # sort user by most recent entry for display 801 | userList.sort(key = lambda userdata:userdata[1]) 802 | userList.reverse() 803 | sortedUsers = [] 804 | userIndex = [] 805 | for user in userList: 806 | sortedUsers.append(user[0]) 807 | userIndex.append(user[2]) 808 | 809 | ans = menu_handler(sortedUsers, "pick a townie to browse their feels, or type 'q' to go home: ", 15, page, SETTINGS.get("rainbows", False), prompt) 810 | 811 | if ans is not False: 812 | (page, choice) = ans 813 | redraw("~{user}'s recorded feels, listed by date: \n".format(user=userIndex[choice])) 814 | view_feels(userIndex[choice]) 815 | view_neighbors(users, prompt, page) 816 | else: 817 | redraw() 818 | return 819 | 820 | def view_feels(townie): 821 | ''' 822 | generates a list of all feels by given townie and displays in 823 | date order; allows selection of one feel to read. 824 | ''' 825 | 826 | metas, owner = generate_feels_list(townie) 827 | 828 | if len(metas) > 0: 829 | entries = [] 830 | for entry in metas: 831 | pub = "" 832 | if core.nopub(entry[0]): 833 | pub = "(nopub)" 834 | entries.append(""+entry[4]+" ("+p.no("word", entry[2])+") "+"\t"+pub) 835 | 836 | return list_entries(metas, entries, owner+" recorded feels, listed by date: ") 837 | else: 838 | redraw("no feels recorded by ~"+townie) 839 | 840 | def generate_feels_list(user): 841 | """create a list of feels for display from the named user. 842 | """ 843 | 844 | filenames = [] 845 | showpub = False 846 | 847 | if user == config.USER: 848 | entryDir = config.MAIN_FEELS 849 | owner = "your" 850 | if core.publishing(): 851 | showpub = True 852 | else: 853 | owner = "~"+user+"'s" 854 | entryDir = os.path.join("/home", user, ".ttbp", "entries") 855 | 856 | for entry in os.listdir(entryDir): 857 | filenames.append(os.path.join(entryDir, entry)) 858 | metas = core.meta(filenames) 859 | metas.sort(key = lambda entry:entry[4]) 860 | metas.reverse() 861 | 862 | return metas, owner 863 | 864 | def backup_feels(): 865 | """creates a tar.gz of user's entries directory""" 866 | 867 | backupfile = os.path.join(os.path.expanduser('~'), "feels-backup-"+time.strftime("%Y%m%d-%H%M%S")+".tar.gz") 868 | 869 | print("""\ 870 | i'm preparing all of your entries for backup.""") 871 | 872 | print("...") 873 | time.sleep(1) 874 | 875 | print(""" 876 | ready to go! a backup file will be saved to your home directory at: 877 | {backuploc}""".format(backuploc=backupfile)) 878 | 879 | ans = util.input_yn("""\ 880 | 881 | would you like to create this backup? 882 | 883 | please enter""") 884 | 885 | if ans: 886 | if not subprocess.call(["tar", "-C", config.PATH, "-czf", backupfile, "entries"]): 887 | subprocess.call(["chmod", "600", backupfile]) 888 | if not os.path.exists(config.BACKUPS): 889 | subprocess.call(["mkdir", config.BACKUPS]) 890 | subprocess.call(["chmod", "700", config.BACKUPS]) 891 | subprocess.call(["cp", backupfile, config.BACKUPS]) 892 | print("\nbackup saved! i also put a copy at {backup_dir} for you.".format(backup_dir = config.BACKUPS)) 893 | else: 894 | print(config.mystery_error) 895 | else: 896 | print("no problem, {friend}; come back whenever if you want a backup!".format(friend=chatter.say("friend"))) 897 | 898 | input("\n\npress to go back to managing your feels.\n\n") 899 | redraw() 900 | 901 | return 902 | 903 | def delete_feels(): 904 | """handles deleting feels one at a time""" 905 | 906 | feel = input("""which day's feels do you want to load for deletion? 907 | 908 | YYYYMMDD (or 'q' to cancel)> """) 909 | 910 | if feel in util.BACKS: 911 | return 912 | 913 | print("...") 914 | time.sleep(0.1) 915 | print("""\ 916 | here's a preview of that feel. press when you're done reviewing! 917 | -------------------------------------------------------------""") 918 | 919 | if subprocess.call(["less", os.path.join(config.MAIN_FEELS, feel+".txt")]): 920 | redraw("deleting feels") 921 | print("""\ 922 | sorry, i couldn't find feels for {date}! 923 | 924 | please try again, or type to cancel. 925 | """.format(date=feel)) 926 | return delete_feels() 927 | 928 | print(""" 929 | ------------------------------------------------------------- 930 | 931 | feels deletion is irreversible! if you're sure you want to delete this feel, 932 | type the date again to confirm, or 'q' to cancel.""") 933 | 934 | confirm = input("[{feeldate}]> ".format(feeldate=feel)) 935 | 936 | if confirm == feel: 937 | print("...") 938 | time.sleep(0.5) 939 | core.delete_feel(feel+".txt") 940 | print("feels deleted!") 941 | else: 942 | print("deletion canceled!") 943 | 944 | ans = util.input_yn("""do you want to delete a different feel? 945 | please enter""") 946 | 947 | if ans: 948 | redraw("deleting feels") 949 | return delete_feels() 950 | else: 951 | print("okay! please come back any time if you want to delete old feels!") 952 | input("\n\npress to go back to managing your feels.\n\n") 953 | redraw() 954 | 955 | return 956 | 957 | 958 | def purge_feels(): 959 | """handles deleting all feels""" 960 | 961 | print(config.feels_purge_prompt) 962 | 963 | print("...") 964 | time.sleep(0.5) 965 | print("...loading feels...") 966 | time.sleep(1) 967 | print("...") 968 | 969 | feelscount = len(os.listdir(config.MAIN_FEELS)) 970 | 971 | if feelscount > 0: 972 | purgecode = util.genID(5) 973 | 974 | print(""" 975 | 976 | i've loaded up all {count} of your feels for purging. if you're ready, carefully 977 | type the following purge code: 978 | _________ 979 | | | 980 | | {purgecode} | 981 | |_______| 982 | """.format(purgecode=purgecode, count=feelscount)) 983 | 984 | ans = input("(leave blank or type anything else to cancel) > ") 985 | 986 | if ans == purgecode: 987 | print("...") 988 | time.sleep(0.5) 989 | unpublish() 990 | 991 | if not subprocess.call(["rm", "-rf", config.MAIN_FEELS]): 992 | subprocess.call(["mkdir", config.MAIN_FEELS]) 993 | core.load_files() 994 | print("ALL FEELS PURGED! you're ready to start fresh!") 995 | else: 996 | print(config.mystery_error) 997 | else: 998 | print("\nfeels purge canceled! you're welcome to come back again.") 999 | else: 1000 | print("you don't have any feels to purge, "+chatter.say("friend")) 1001 | 1002 | input("\n\npress to go back to managing your feels.\n\n") 1003 | redraw() 1004 | 1005 | return 1006 | 1007 | def wipe_account(): 1008 | """handles wiping feels account""" 1009 | 1010 | print(config.account_wipe_prompt) 1011 | 1012 | print("...") 1013 | time.sleep(0.5) 1014 | print("...packaging up all your feels...") 1015 | time.sleep(1) 1016 | print("...") 1017 | purgecode = util.genID(5) 1018 | 1019 | print(""" 1020 | your account is all packed up! if you're ready, carefully type the following 1021 | purge code: 1022 | _________ 1023 | | | 1024 | | {purgecode} | 1025 | |_______| 1026 | """.format(purgecode=purgecode)) 1027 | 1028 | ans = input("(leave blank or type anything else to cancel) > ") 1029 | 1030 | if ans == purgecode: 1031 | print("...") 1032 | time.sleep(0.5) 1033 | unpublish() 1034 | 1035 | if core.publishing(): 1036 | publishDir = os.path.join(config.PUBLIC, SETTINGS.get("publish dir")) 1037 | make_publish_dir(publishDir) 1038 | 1039 | 1040 | if not subprocess.call(["rm", "-rf", config.PATH]): 1041 | print(""" 1042 | account deleted! if you ever want to come back, you're always welcome to start 1043 | fresh :) 1044 | 1045 | thank you for sharing your feels!""") 1046 | input("\n\npress to exit the feels engine.\n\n") 1047 | sys.exit(stop()) 1048 | else: 1049 | print(config.mystery_error) 1050 | else: 1051 | print("\naccount deletion canceled! you're welcome to come back again.") 1052 | 1053 | input("\n\npress to go back to managing your feels.\n\n") 1054 | redraw() 1055 | 1056 | return 1057 | 1058 | def load_backup(): 1059 | """ 1060 | scans for archive files (prompting for a file location if not found), opens, 1061 | copies files to main feels directory (skipping ones that already exist) 1062 | """ 1063 | 1064 | print("scanning backup directory at {directory}...".format(directory=config.BACKUPS)) 1065 | 1066 | time.sleep(.5) 1067 | print("...\n") 1068 | 1069 | backups = [] 1070 | 1071 | try: 1072 | for filename in os.listdir(config.BACKUPS): 1073 | if "feels-backup" in filename and ".tar" in filename: 1074 | backups.append(filename) 1075 | except FileNotFoundError: 1076 | subprocess.call(["mkdir", config.BACKUPS]) 1077 | 1078 | if len(backups) < 1: 1079 | print(""" 1080 | sorry, i didn't find any feels backups! if you have a backup file handy, please 1081 | move it to {directory} and try running this tool again.\ 1082 | """.format(directory=config.BACKUPS)) 1083 | else: 1084 | print("backup files found:\n") 1085 | ans = menu_handler(backups, "pick a backup file to load (or 'q' to cancel): ", 15, 0, SETTINGS.get("rainbows", False), "backup files found:") 1086 | 1087 | if ans is not False: 1088 | (page, choice) = ans 1089 | imports = core.process_backup(os.path.join(config.BACKUPS, backups[choice])) 1090 | for feel in imports: 1091 | print("importing {entry}".format(entry="-".join(util.parse_date(feel)))) 1092 | subprocess.call(["mv", feel, config.MAIN_FEELS]) 1093 | time.sleep(.01) 1094 | 1095 | core.load_files() 1096 | 1097 | tempdir = os.path.join(config.BACKUPS, os.path.splitext(os.path.splitext(os.path.basename(backups[choice]))[0])[0], "entries") 1098 | 1099 | time.sleep(.5) 1100 | print("...\n") 1101 | 1102 | if len(os.listdir(tempdir)) == 0: 1103 | os.rmdir(tempdir) 1104 | print("congrats! your feels archive has been unloaded.") 1105 | else: 1106 | print("""\ 1107 | i've unloaded as much as i can, but there are still some feels i didn't copy 1108 | over. this is probably because you have current feels on the same days, and i 1109 | didn't want to overwrite them. 1110 | 1111 | you can check out the leftover feels yourself at: 1112 | 1113 | {directory}""".format(directory=tempdir)) 1114 | else: 1115 | return 1116 | 1117 | input("\n\npress to go back to managing your feels.\n\n") 1118 | return 1119 | 1120 | def bury_feels(): 1121 | """queries for a feel to bury, then calls the feels burying handler. 1122 | """ 1123 | 1124 | feel = input(config.bury_feels_prompt) 1125 | 1126 | if feel in util.BACKS: 1127 | return 1128 | 1129 | print("...") 1130 | time.sleep(0.1) 1131 | print("""\ 1132 | here's a preview of that feel. press when you're done reviewing! 1133 | -------------------------------------------------------------""") 1134 | 1135 | if subprocess.call(["less", os.path.join(config.MAIN_FEELS, feel+".txt")]): 1136 | redraw("burying feels") 1137 | print("""\ 1138 | sorry, i couldn't find feels for {date}! 1139 | 1140 | please try again, or type to cancel. 1141 | """.format(date=feel)) 1142 | return delete_feels() 1143 | 1144 | print(""" 1145 | ------------------------------------------------------------- 1146 | 1147 | feels burying is irreversible! if you're sure you want to bury this feel, 1148 | type the date again to confirm, or 'q' to cancel. 1149 | """) 1150 | 1151 | confirm = input("[{feeldate}]> ".format(feeldate=feel)) 1152 | 1153 | if confirm == feel: 1154 | print("...") 1155 | time.sleep(0.5) 1156 | core.bury_feel(feel+".txt") 1157 | print("feels buried!") 1158 | else: 1159 | print("burying canceled!") 1160 | 1161 | ans = util.input_yn("""do you want to bury a different feel? please enter""") 1162 | 1163 | if ans: 1164 | redraw("burying feels") 1165 | return bury_feels() 1166 | else: 1167 | print("okay! please come back any time if you want to bury your feels!") 1168 | input("\n\npress to go back to managing your feels.\n\n") 1169 | redraw() 1170 | 1171 | return 1172 | 1173 | def show_credits(): 1174 | ''' 1175 | prints author acknowledgements and commentary 1176 | ''' 1177 | 1178 | print(config.credits) 1179 | input("\n\npress to go back home.\n\n") 1180 | redraw() 1181 | 1182 | return 1183 | 1184 | ## handlers 1185 | 1186 | def write_entry(entry=os.path.join(config.MAIN_FEELS, "test.txt")): 1187 | ''' 1188 | main feels-recording handler 1189 | ''' 1190 | 1191 | entered = input(config.recording) 1192 | 1193 | if entered: 1194 | entryFile = open(entry, "a") 1195 | entryFile.write("\n"+entered+"\n") 1196 | entryFile.close() 1197 | subprocess.call([SETTINGS.get("editor"), entry]) 1198 | 1199 | left = "" 1200 | 1201 | core.load_files() 1202 | 1203 | if SETTINGS.get("post as nopub"): 1204 | core.toggle_nopub(os.path.basename(entry)) 1205 | else: 1206 | if core.publishing(): 1207 | core.write_html("index.html") 1208 | left = "posted to {url}/index.html\n\n> ".format( 1209 | url="/".join( 1210 | [config.LIVE+config.USER, 1211 | str(SETTINGS.get("publish dir"))])) 1212 | 1213 | if SETTINGS.get('gopher'): 1214 | gopher.publish_gopher('feels', core.FILES) 1215 | left += "also posted to your ~/public_gopher!\n\n> " 1216 | 1217 | #core.load_files() 1218 | redraw(left + "thanks for sharing your feels!") 1219 | 1220 | return 1221 | 1222 | def list_nopubs(user): 1223 | ''' 1224 | handler for toggling nopub on individual entries 1225 | ''' 1226 | 1227 | metas, owner = generate_feels_list(user) 1228 | 1229 | if len(metas) > 0: 1230 | return set_nopubs(metas, user, "publishing status of your feels:") 1231 | else: 1232 | redraw("no feels recorded by ~"+user) 1233 | 1234 | def set_nopubs(metas, user, prompt, page=0): 1235 | """displays a list of entries for pub/nopub toggling. 1236 | """ 1237 | 1238 | if core.publishing(): 1239 | nopub_note = "" 1240 | else: 1241 | nopub_note = """\ 1242 | (since you're not publishing your entries, these settings don't really matter; 1243 | none of your feels will be viewable outside of this server)""" 1244 | print(nopub_note + "\n") 1245 | 1246 | entries = [] 1247 | for entry in metas: 1248 | pub = "" 1249 | if core.nopub(entry[0]): 1250 | pub = "(nopub)" 1251 | entries.append(""+entry[4]+" ("+p.no("word", entry[2])+") "+"\t"+pub) 1252 | 1253 | ans = menu_handler(entries, "pick an entry from the list to toggle nopub status, or type 'q' to go back: ", 10, page, SETTINGS.get("rainbows", False), prompt+"\n\n"+nopub_note) 1254 | 1255 | if ans is not False: 1256 | (page, choice) = ans 1257 | target = os.path.basename(metas[choice][0]) 1258 | action = core.toggle_nopub(target) 1259 | redraw(prompt) 1260 | 1261 | if SETTINGS["gopher"]: 1262 | gopher.publish_gopher('feels', core.get_files()) 1263 | 1264 | return set_nopubs(metas, user, prompt, page) 1265 | 1266 | else: 1267 | redraw() 1268 | return 1269 | 1270 | def send_feedback(entered, subject="none"): 1271 | ''' 1272 | main feedback/bug report handler 1273 | ''' 1274 | 1275 | message = "" 1276 | 1277 | temp = tempfile.NamedTemporaryFile() 1278 | if entered: 1279 | msgFile = open(temp.name, "a") 1280 | msgFile.write(entered+"\n") 1281 | msgFile.close() 1282 | subprocess.call([SETTINGS["editor"], temp.name]) 1283 | message = open(temp.name, 'r').read() 1284 | 1285 | if message: 1286 | id = "#"+util.genID(3) 1287 | mail = MIMEText(message) 1288 | mail['To'] = config.FEEDBOX 1289 | mail['From'] = config.USER+"@tilde.town" 1290 | mail['Subject'] = " ".join(["[ttbp]", subject, id]) 1291 | m = os.popen("/usr/sbin/sendmail -t -oi", 'w') 1292 | m.write(mail.as_string()) 1293 | m.close() 1294 | 1295 | exit = """\ 1296 | thanks for writing! for your reference, it's been recorded 1297 | > as """+ " ".join([subject, id])+""". i'll try to respond to you soon.\ 1298 | """ 1299 | else: 1300 | exit = """\ 1301 | i didn't send your blank message. if you made a mistake, please try 1302 | running through the feedback option again!\ 1303 | """ 1304 | 1305 | return exit 1306 | 1307 | def list_entries(metas, entries, prompt, page=0): 1308 | ''' 1309 | displays a list of entries for reading selection, allowing user to select 1310 | one for display. 1311 | ''' 1312 | 1313 | ans = menu_handler(entries, "pick an entry from the list, or type 'q' to go back: ", 10, page, SETTINGS.get("rainbows", False), prompt) 1314 | 1315 | if ans is not False: 1316 | (page, choice) = ans 1317 | redraw("now reading ~{user}'s feels on {date}\n> press to return to feels list.\n\n".format(user=metas[choice][5], 1318 | date=metas[choice][4])) 1319 | 1320 | show_entry(metas[choice][0]) 1321 | redraw(prompt) 1322 | 1323 | return list_entries(metas, entries, prompt, page) 1324 | 1325 | else: 1326 | redraw() 1327 | return 1328 | 1329 | def show_entry(filename): 1330 | ''' 1331 | call less on passed in filename 1332 | ''' 1333 | 1334 | subprocess.call(["less", filename]) 1335 | 1336 | return 1337 | 1338 | def view_global_feed(): 1339 | ''' 1340 | display list of most recent global entries 1341 | ''' 1342 | 1343 | (entries, metas)= feed_list(core.find_ttbps()) 1344 | list_entries(metas, entries, "recent global entries:") 1345 | redraw() 1346 | 1347 | return 1348 | 1349 | def view_subscribed_feed(subs, prompt=""): 1350 | ''' 1351 | display list of most recent entries on user's subscribed list. 1352 | ''' 1353 | (entries, metas)= feed_list(subs, 0) 1354 | list_entries(metas, entries, prompt) 1355 | redraw() 1356 | 1357 | return 1358 | 1359 | def feed_list(townies, delta=30): 1360 | ''' 1361 | given a list of townies, generate a list of 50 most recent entries within 1362 | given interval (default 30 days; 0 days for no limit). validates against 1363 | townies with ttbp config files. 1364 | 1365 | returns a tuple of (entries, metas) 1366 | ''' 1367 | 1368 | feedList = [] 1369 | all_users = core.find_ttbps() 1370 | 1371 | for townie in townies: 1372 | if townie not in all_users: 1373 | continue 1374 | 1375 | entryDir = os.path.join("/home", townie, ".ttbp", "entries") 1376 | try: 1377 | filenames = os.listdir(entryDir) 1378 | except OSError: 1379 | filenames = [] 1380 | 1381 | for entry in filenames: 1382 | if delta > 0: 1383 | if core.valid(entry): 1384 | year = int(entry[0:4]) 1385 | month = int(entry[4:6]) 1386 | day = int(entry[6:8]) 1387 | datecheck = datetime.date(year, month, day) 1388 | displayCutoff = datetime.date.today() - datetime.timedelta(days=delta) 1389 | 1390 | if datecheck > displayCutoff: 1391 | feedList.append(os.path.join(entryDir, entry)) 1392 | else: 1393 | feedList.append(os.path.join(entryDir, entry)) 1394 | 1395 | metas = core.meta(feedList) 1396 | metas.sort(key = lambda entry:entry[3]) 1397 | metas.reverse() 1398 | 1399 | entries = [] 1400 | for entry in metas[0:50]: 1401 | pad = "" 1402 | if len(entry[5]) < 8: 1403 | pad = "\t" 1404 | 1405 | entries.append("~{user}{pad}\ton {date} ({wordcount})".format( 1406 | user=entry[5], pad=pad, date=entry[3], 1407 | wordcount=p.no("word", entry[2]))) 1408 | 1409 | return entries, metas 1410 | 1411 | def subscription_manager(subs, intro=""): 1412 | ''' 1413 | ''' 1414 | 1415 | menuOptions = [ 1416 | "add pals", 1417 | "remove pals" 1418 | ] 1419 | 1420 | util.print_menu(menuOptions, SETTINGS.get("rainbows", False)) 1421 | 1422 | choice = util.list_select(menuOptions, "what do you want to do? (enter 'q' to go back) ") 1423 | 1424 | top = "" 1425 | 1426 | if choice is not False: 1427 | if choice == 0: 1428 | prompt = "list of townies recording feels:" 1429 | redraw(prompt) 1430 | subs = subscribe_handler(subs, prompt) 1431 | elif choice == 1: 1432 | prompt = "list of townies you're subscribed to:" 1433 | redraw(prompt) 1434 | subs = unsubscribe_handler(subs, prompt) 1435 | else: 1436 | redraw() 1437 | return 1438 | 1439 | redraw(top+intro) 1440 | return subscription_manager(subs, intro) 1441 | 1442 | def unsubscribe_handler(subs, prompt, page=0): 1443 | ''' 1444 | displays a list of currently subscribed users and toggles deletion. 1445 | ''' 1446 | 1447 | subs.sort() 1448 | 1449 | ans = menu_handler(subs, "pick a pal to unsubscribe (or 'q' to cancel): ", 15, page, SETTINGS.get("rainbows", False), "list of townies recording feels:") 1450 | 1451 | if ans is not False: 1452 | (page,choice) = ans 1453 | townie = subs[choice] 1454 | subs.remove(townie) 1455 | save_subs(subs) 1456 | redraw("{townie} removed! \n\n> {prompt}".format(townie=townie, prompt=prompt)) 1457 | return unsubscribe_handler(subs, prompt, page) 1458 | else: 1459 | redraw() 1460 | return subs 1461 | 1462 | def subscribe_handler(subs, prompt, page=0): 1463 | ''' 1464 | displays a list of all users not subscribed to and toggles adding, 1465 | returning the subs list when finished. 1466 | ''' 1467 | 1468 | candidates = [] 1469 | 1470 | for townie in core.find_ttbps(): 1471 | if townie not in subs: 1472 | candidates.append(townie) 1473 | 1474 | candidates.sort() 1475 | 1476 | ans = menu_handler(candidates, "pick a townie to add to your subscriptions (or 'q' to cancel): ", 15, page, SETTINGS.get("rainbows", False), "list of townies recording feels:") 1477 | 1478 | if ans is not False: 1479 | (page, choice) = ans 1480 | townie = candidates[choice] 1481 | subs.append(townie) 1482 | save_subs(subs) 1483 | redraw("{townie} added! \n\n> {prompt}".format(townie=townie, prompt=prompt)) 1484 | return subscribe_handler(subs, prompt, page) 1485 | else: 1486 | redraw() 1487 | return subs 1488 | 1489 | def save_subs(subs): 1490 | ''' 1491 | takes given subscription list and saves it into the user config, 1492 | overwriting whatever is already there. 1493 | ''' 1494 | 1495 | subs_file = open(config.SUBS, 'w') 1496 | 1497 | for townie in subs: 1498 | subs_file.write(townie + "\n") 1499 | subs_file.close() 1500 | 1501 | def graffiti_handler(): 1502 | ''' 1503 | Main graffiti handler; checks for lockfile from another editing sesison 1504 | (overwriting by default if the lock is more than three days old. 1505 | ''' 1506 | 1507 | if os.path.isfile(config.WALL_LOCK) and \ 1508 | time.time() - os.path.getmtime(config.WALL_LOCK) < 60*60*24*3: 1509 | redraw("sorry, {friend}, but someone's there right now. try again in a few!".format(friend=chatter.say("friend"))) 1510 | else: 1511 | subprocess.call(["touch", config.WALL_LOCK]) 1512 | redraw() 1513 | print("""\ 1514 | the graffiti wall is a world-writeable text file. anyone can 1515 | scribble on it; anyone can move or delete things. please be 1516 | considerate of your neighbors when writing on it. 1517 | 1518 | no one will be able to visit the wall while you are here, so don't 1519 | worry about overwriting someone else's work. anything you do to the 1520 | wall will be recorded if you save the file, and you can cancel 1521 | your changes by exiting without saving. 1522 | 1523 | """) 1524 | input("press to visit the wall\n\n") 1525 | subprocess.call([SETTINGS.get("editor"), config.WALL]) 1526 | subprocess.call(["rm", config.WALL_LOCK]) 1527 | redraw("thanks for visiting the graffiti wall!") 1528 | 1529 | 1530 | ## misc helpers 1531 | 1532 | def toggle_pub_default(): 1533 | """setup helper for setting default publish privacy (does not apply 1534 | retroactively). """ 1535 | 1536 | if SETTINGS.get("post as nopub", False) is True: 1537 | (nopub, will) = ("(nopub)", "won't") 1538 | else: 1539 | (nopub, will) = ("public", "will") 1540 | 1541 | if SETTINGS.get("publishing", False) is True: 1542 | publishing = "" 1543 | else: 1544 | publishing = """\ 1545 | since you're currently not publishing your posts to html/gopher, this setting 1546 | won't affect the visibility of your posts. however, the option is still here if 1547 | you'd like to change it. 1548 | """ 1549 | 1550 | print(""" 1551 | 1552 | DEFAULT POST PRIVACY 1553 | 1554 | your entries are set to automatically post as {nopub}. this means they {will} be 1555 | posted to your world-visible pages at first (which you can always change after 1556 | the fact.) 1557 | 1558 | this setting only affects subsequent posts; it does not apply retroactively. 1559 | 1560 | {publishing}""".format(nopub=nopub, will=will, publishing=publishing)) 1561 | 1562 | ans = util.input_yn("""\ 1563 | would you like to change this behavior? 1564 | 1565 | please enter""") 1566 | 1567 | if ans: 1568 | return not SETTINGS.get("post as nopub") 1569 | else: 1570 | return SETTINGS.get("post as nopub") 1571 | 1572 | def toggle_rainbows(): 1573 | """setup helper for rainbow toggling 1574 | """ 1575 | 1576 | if SETTINGS.get("rainbows", False) is True: 1577 | status = "enabled" 1578 | else: 1579 | status = "disabled" 1580 | 1581 | print("\nRAINBOW MENU TOGGLING") 1582 | print("rainbow menus are currently {status}".format(status=status)) 1583 | 1584 | publish = util.input_yn("""\ 1585 | 1586 | would you like to have rainbow menus? 1587 | 1588 | please enter\ 1589 | """) 1590 | 1591 | return publish 1592 | 1593 | def select_editor(): 1594 | ''' 1595 | setup helper for editor selection 1596 | ''' 1597 | 1598 | print("\nTEXT EDITOR SELECTION") 1599 | print("your current editor is: "+SETTINGS.get("editor")) 1600 | util.print_menu(EDITORS, SETTINGS.get("rainbows", False)) 1601 | choice = util.list_select(EDITORS, "pick your favorite text editor, or type 'q' to go back: ") 1602 | 1603 | if choice is False: 1604 | # if selection is canceled, return either previously set editor or default 1605 | return SETTINGS.get("editor", EDITORS[0]) 1606 | return EDITORS[int(choice)] 1607 | 1608 | def select_publish_dir(): 1609 | ''' 1610 | setup helper for publish directory selection 1611 | ''' 1612 | 1613 | if not core.publishing(): 1614 | return None 1615 | 1616 | current = SETTINGS.get("publish dir") 1617 | republish = False 1618 | 1619 | print("\nUPDATING HTML PATH") 1620 | 1621 | if current: 1622 | print("\ncurrent publish dir:\t"+os.path.join(config.PUBLIC, SETTINGS["publish dir"])) 1623 | republish = True 1624 | 1625 | choice = input("\nwhere do you want your blog published? (leave blank to use default \"blog\") ") 1626 | if not choice: 1627 | choice = "blog" 1628 | 1629 | publishDir = os.path.join(config.PUBLIC, choice) 1630 | while os.path.exists(publishDir): 1631 | second = input(""" 1632 | {pDir} already exists! 1633 | 1634 | setting this as your publishing directory means this program may 1635 | delete or overwrite file there! 1636 | 1637 | if you're sure you want to use it, hit to confirm. 1638 | otherwise, pick another location: """.format(pDir=publishDir)) 1639 | 1640 | if second == "": 1641 | break 1642 | choice = second 1643 | publishDir = os.path.join(config.PUBLIC, choice) 1644 | 1645 | return choice 1646 | 1647 | def select_publishing(): 1648 | ''' 1649 | setup helper for toggling publishing 1650 | ''' 1651 | 1652 | publish = util.input_yn("""\ 1653 | 1654 | SETTING UP PUBLISHING 1655 | 1656 | do you want to publish your feels online? 1657 | 1658 | if yes, your feels will be published to a directory of your choice in 1659 | your public_html. i'll confirm the location of that directory in a 1660 | moment. 1661 | 1662 | if not, your feels will only be readable from within the tilde.town 1663 | network. if you already have a publishing directory, i'll remove it for 1664 | you (don't worry, your written entries will still be saved!) 1665 | 1666 | you can change this option any time. 1667 | 1668 | please enter\ 1669 | """) 1670 | 1671 | return publish 1672 | 1673 | def unpublish(): 1674 | '''remove all published entries in html and gopher. 1675 | ''' 1676 | 1677 | global SETTINGS 1678 | 1679 | directory = SETTINGS.get("publish dir") 1680 | 1681 | if directory: 1682 | publishDir = os.path.join(config.PUBLIC, directory) 1683 | if os.path.exists(publishDir): 1684 | subprocess.call(["rm", "-rf", publishDir]) 1685 | subprocess.call(["rm", "-rf", config.WWW]) 1686 | make_publish_dir(SETTINGS.get("publish dir")) 1687 | #SETTINGS.update({"publish dir": None}) 1688 | 1689 | if SETTINGS.get("gopher"): 1690 | gopher.unpublish() 1691 | 1692 | def update_publishing(): 1693 | ''' 1694 | updates publishing directory if user is publishing. otherwise, wipe it. 1695 | ''' 1696 | 1697 | global SETTINGS 1698 | 1699 | if core.publishing(): 1700 | oldDir = SETTINGS.get("publish dir") 1701 | newDir = select_publish_dir() 1702 | SETTINGS.update({"publish dir": newDir}) 1703 | if oldDir: 1704 | subprocess.call(["rm", os.path.join(config.PUBLIC, oldDir)]) 1705 | make_publish_dir(newDir) 1706 | core.load_files() 1707 | #core.write_html("index.html") 1708 | else: 1709 | unpublish() 1710 | 1711 | core.load(SETTINGS) 1712 | 1713 | def make_publish_dir(publish_dir): 1714 | ''' 1715 | setup helper to create publishing directory 1716 | ''' 1717 | 1718 | if not os.path.exists(config.WWW): 1719 | subprocess.call(["mkdir", config.WWW]) 1720 | subprocess.call(["ln", "-s", os.path.join(config.USER_CONFIG, "style.css"), os.path.join(config.WWW, "style.css")]) 1721 | subprocess.call(["touch", os.path.join(config.WWW, "index.html")]) 1722 | index = open(os.path.join(config.WWW, "index.html"), "w") 1723 | index.write("

ttbp blog placeholder

") 1724 | index.close() 1725 | 1726 | if core.publishing(): 1727 | live = os.path.join(config.PUBLIC, publish_dir) 1728 | if os.path.exists(live): 1729 | subprocess.call(["rm", live]) 1730 | 1731 | subprocess.call(["ln", "-s", config.WWW, live]) 1732 | 1733 | return "\n\tpublishing to "+config.LIVE+config.USER+"/"+SETTINGS.get("publish dir")+"/\n\n" 1734 | 1735 | else: 1736 | return "" 1737 | #print("\n\tpublishing to "+config.LIVE+config.USER+"/"+SETTINGS.get("publish dir")+"/\n\n") 1738 | 1739 | def update_gopher(): 1740 | ''' 1741 | helper for toggling gopher settings 1742 | ''' 1743 | # TODO for now i'm hardcoding where people's gopher stuff is generated. if 1744 | # there is demand for this to be configurable we can expose that. 1745 | if SETTINGS.get("gopher"): 1746 | gopher.setup_gopher('feels') 1747 | gopher.publish_gopher("feels", core.get_files()) 1748 | else: 1749 | subprocess.call(["rm", config.GOPHER_PATH]) 1750 | redraw("gopher publishing set to {gopher}".format(gopher=SETTINGS.get("gopher"))) 1751 | 1752 | ##### PATCHING UTILITIES 1753 | 1754 | def user_up_to_date(): 1755 | ''' 1756 | checks to see if current user is up to the same version as system 1757 | ''' 1758 | 1759 | versionFile = os.path.join(config.PATH, "version") 1760 | if not os.path.exists(versionFile): 1761 | return False 1762 | 1763 | ver = open(versionFile, "r").read() 1764 | 1765 | if ver == __version__: 1766 | return True 1767 | 1768 | return False 1769 | 1770 | def update_user_version(): 1771 | ''' 1772 | updates user to current version, printing relevant release notes and 1773 | stepping through new features. 1774 | ''' 1775 | 1776 | global SETTINGS 1777 | 1778 | versionFile = os.path.join(config.PATH, "version") 1779 | 1780 | print("ttbp had some updates!") 1781 | 1782 | print("\ngive me a second to update you to version "+__version__+"...\n") 1783 | 1784 | time.sleep(1) 1785 | print("...") 1786 | time.sleep(0.5) 1787 | 1788 | userVersion = "" 1789 | (x, y, z) = [0, 0, 0] 1790 | 1791 | if not os.path.isfile(versionFile): 1792 | # updates from 0.8.5 to 0.8.6, before versionfile existed 1793 | 1794 | # change style.css location 1795 | if core.publishing(): 1796 | if os.path.isfile(os.path.join(config.WWW, "style.css")): 1797 | subprocess.call(["mv", os.path.join(config.WWW, "style.css"), config.USER_CONFIG]) 1798 | 1799 | # change www symlink 1800 | if os.path.exists(config.WWW): 1801 | subprocess.call(["rm", config.WWW]) 1802 | 1803 | subprocess.call(["mkdir", config.WWW]) 1804 | 1805 | subprocess.call(["ln", "-s", os.path.join(config.USER_CONFIG, "style.css"), os.path.join(config.WWW, "style.css")]) 1806 | 1807 | publishDir = os.path.join(config.PUBLIC, SETTINGS.get("publish dir")) 1808 | if os.path.exists(publishDir): 1809 | subprocess.call(["rm", "-rf", publishDir]) 1810 | 1811 | subprocess.call(["ln", "-s", config.WWW, os.path.join(config.PUBLIC, SETTINGS.get("publish dir"))]) 1812 | 1813 | # repopulate html files 1814 | core.load_files() 1815 | #core.write_html("index.html") 1816 | 1817 | # add publishing setting 1818 | print("\nnew feature!\n") 1819 | SETTINGS.update({"publishing":select_publishing()}) 1820 | update_publishing() 1821 | ttbprc = open(config.TTBPRC, "w") 1822 | ttbprc.write(json.dumps(SETTINGS, sort_keys=True, indent=2, separators=(',',':'))) 1823 | ttbprc.close() 1824 | 1825 | else: # version at least 0.8.6 1826 | userVersion = open(versionFile, "r").read().rstrip() 1827 | x, y, z = [int(num) for num in userVersion.split(".")] 1828 | 1829 | # from 0.8.6 1830 | if userVersion == "0.8.6": 1831 | print("\nresetting your publishing settings...\n") 1832 | SETTINGS.update({"publishing":select_publishing()}) 1833 | update_publishing() 1834 | ttbprc = open(config.TTBPRC, "w") 1835 | ttbprc.write(json.dumps(SETTINGS, sort_keys=True, indent=2, separators=(',',':'))) 1836 | ttbprc.close() 1837 | 1838 | # from earlier than 0.10.1 1839 | if y < 10: 1840 | # select gopher 1841 | print("[ NEW FEATURE ]") 1842 | print(""" 1843 | * thanks to help from ~vilmibm, ttbp now supports publishing to gopher! 1844 | * if you enable gopher publishing, feels will automatically publish to 1845 | gopher://tilde.town/1/~"""+config.USER+"""/feels 1846 | 1847 | """) 1848 | SETTINGS.update({'gopher': gopher.select_gopher()}) 1849 | if SETTINGS.get('gopher'): 1850 | print("opting into gopher: " + str(SETTINGS['gopher'])) 1851 | gopher.setup_gopher('feels') 1852 | else: 1853 | print("okay, passing on gopher for now. this option is available in settings if you change\nyour mind!") 1854 | 1855 | if y < 11: 1856 | # set rainbow menu for 0.11.0 1857 | print("[ NEW FEATURE ]") 1858 | SETTINGS.update({"rainbows": toggle_rainbows()}) 1859 | 1860 | if z < 2: 1861 | # set default option for 0.11.2 1862 | # print("default nopub: false") 1863 | SETTINGS.update({"post as nopub": False}) 1864 | save_settings() 1865 | 1866 | if z < 3: 1867 | # update permalink css 1868 | style = open(os.path.join(config.USER_CONFIG, 'style.css'), 'r').read() 1869 | if "permalink" not in style: 1870 | print("adding new css...") 1871 | with open(os.path.join(config.USER_CONFIG, 'style.css'), 'a') as f: 1872 | f.write(""" 1873 | .entry p.permalink { 1874 | font-size: .6em; 1875 | font-color: #808080; 1876 | text-align: right; 1877 | }""") 1878 | 1879 | print(""" 1880 | you're all good to go, """+chatter.say("friend")+"""! please contact ~endorphant if 1881 | something strange happened to you during this update. 1882 | """) 1883 | 1884 | if y < 10: 1885 | # version 0.10.1 patch notes 1886 | print(config.UPDATES["0.10.1"]) 1887 | 1888 | if y < 11: 1889 | # version 0.11.0 patch notes 1890 | print(config.UPDATES["0.11.0"]) 1891 | 1892 | if y < 11 and z < 1: 1893 | # version 0.11.1 patch notes 1894 | print(config.UPDATES["0.11.1"]) 1895 | 1896 | if y < 11 and z < 2: 1897 | # version 0.11.2 patch notes 1898 | print(config.UPDATES["0.11.2"]) 1899 | 1900 | if y < 11 and z < 3: 1901 | # version 0.11.3 patch notes 1902 | print(config.UPDATES["0.11.3"]) 1903 | 1904 | if y < 12: 1905 | # version 0.12.0 patch notes 1906 | print(config.UPDATES["0.12.0"]) 1907 | 1908 | if z < 1: 1909 | # version 0.12.1 patch notes 1910 | print(config.UPDATES["0.12.1"]) 1911 | 1912 | if z < 2: 1913 | # version 0.12.2 patch notes 1914 | print(config.UPDATES["0.12.2"]) 1915 | 1916 | confirm = "" 1917 | 1918 | while confirm not in ("x", "", "X", ""): 1919 | confirm = input("\nplease type when you've finished reading about the updates! ") 1920 | 1921 | print("\n\n") 1922 | 1923 | open(versionFile, "w").write(__version__) 1924 | 1925 | ##### 1926 | 1927 | if __name__ == '__main__': 1928 | sys.exit(main()) 1929 | -------------------------------------------------------------------------------- /ttbp/update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import core 4 | 5 | core.load() 6 | print("\n blog updated at "+core.write("index.html")+"\n") 7 | -------------------------------------------------------------------------------- /ttbp/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | util.py: frequently used terminal and text processing utilities 5 | copyright (c) 2016 ~endorphant (endorphant@tilde.town) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | ''' 26 | import random 27 | import time 28 | from six.moves import input 29 | import os 30 | 31 | import colorama 32 | import inflect 33 | 34 | ## misc globals 35 | BACKS = ['back', 'b', 'q', ''] 36 | NAVS = ['u', 'd'] 37 | 38 | ## color stuff 39 | colorama.init() 40 | textcolors = [ colorama.Fore.RED, colorama.Fore.GREEN, colorama.Fore.YELLOW, colorama.Fore.BLUE, colorama.Fore.MAGENTA, colorama.Fore.WHITE, colorama.Fore.CYAN] 41 | lastcolor = colorama.Fore.RESET 42 | 43 | p = inflect.engine() 44 | 45 | def set_rainbow(): 46 | ''' 47 | prints a random terminal color code 48 | ''' 49 | 50 | global lastcolor 51 | 52 | color = lastcolor 53 | while color == lastcolor: 54 | color = random.choice(textcolors) 55 | 56 | lastcolor = color 57 | 58 | print(color) 59 | 60 | def reset_color(): 61 | ''' 62 | prints terminal color code reset 63 | ''' 64 | 65 | print(colorama.Fore.RESET) 66 | 67 | def attach_rainbow(): 68 | ''' 69 | returns a random terminal color code, presumably to be 'attached' to a string 70 | ''' 71 | 72 | global lastcolor 73 | 74 | color = lastcolor 75 | while color == lastcolor: 76 | color = random.choice(textcolors) 77 | 78 | lastcolor = color 79 | return color 80 | 81 | def attach_reset(): 82 | ''' 83 | returns terminal color code reset, presumably to be 'attached' to a string 84 | ''' 85 | 86 | return colorama.Style.RESET_ALL 87 | 88 | def hilight(text): 89 | ''' 90 | takes a string and highlights it on return 91 | ''' 92 | 93 | return colorama.Style.BRIGHT+text+colorama.Style.NORMAL 94 | 95 | def rainbow(txt): 96 | ''' 97 | Takes a string and makes every letter a different color. 98 | ''' 99 | 100 | rainbow = "" 101 | for letter in txt: 102 | rainbow += attach_rainbow() + letter 103 | 104 | rainbow += attach_reset() 105 | 106 | return rainbow 107 | 108 | def pretty_time(time): 109 | ''' 110 | human-friendly time formatter 111 | 112 | takes an integer number of seconds and returns a phrase that describes it, 113 | using the largest possible figure, rounded down (ie, time=604 returns '10 114 | minutes', not '10 minutes, 4 seconds' or '604 seconds') 115 | ''' 116 | 117 | m, s = divmod(time, 60) 118 | if m > 0: 119 | h, m = divmod(m, 60) 120 | if h > 0: 121 | d, h = divmod(h, 24) 122 | if d > 0: 123 | w, d = divmod(d, 7) 124 | if w > 0: 125 | mo, w = divmod(w, 4) 126 | if mo > 0: 127 | return p.no("month", mo) 128 | else: 129 | return p.no("week", w) 130 | else: 131 | return p.no("day", d) 132 | else: 133 | return p.no("hour", h) 134 | else: 135 | return p.no("minute", m) 136 | else: 137 | return p.no("second", s) 138 | 139 | def genID(digits=5): 140 | ''' 141 | returns a string-friendly string of digits, which can start with 0 142 | ''' 143 | 144 | id = "" 145 | x = 0 146 | while x < digits: 147 | id += str(random.randint(0,9)) 148 | x += 1 149 | 150 | return id 151 | 152 | def print_menu(menu, rainbow=False): 153 | ''' 154 | A pretty menu handler that takes an incoming lists of 155 | options and prints them nicely. 156 | 157 | Set rainbow=True for colorized menus. 158 | ''' 159 | 160 | i = 0 161 | for x in menu: 162 | line = [] 163 | if rainbow is not False: 164 | line.append(attach_rainbow()) 165 | line.append("\t[ ") 166 | if i < 10: 167 | line.append(" ") 168 | line.append(str(i)+" ] "+x) 169 | line.append(attach_reset()) 170 | print("".join(line)) 171 | i += 1 172 | 173 | def list_select(options, prompt): 174 | ''' 175 | Given a list and query prompt, returns either False as an 176 | eject flag, or an integer index of the list Catches cancel 177 | option from list defined by BACKS; otherwise, retries on 178 | ValueError or IndexError. 179 | ''' 180 | 181 | ans = "" 182 | invalid = True 183 | 184 | choice = input("\n"+prompt) 185 | 186 | if choice in BACKS: 187 | return False 188 | 189 | if choice in NAVS: 190 | return choice 191 | 192 | try: 193 | ans = int(choice) 194 | except ValueError: 195 | return list_select(options, prompt) 196 | 197 | try: 198 | options[ans] 199 | except IndexError: 200 | return list_select(options, prompt) 201 | 202 | return ans 203 | 204 | def input_yn(query): 205 | ''' 206 | Given a query, returns boolean True or False by processing y/n input 207 | ''' 208 | 209 | try: 210 | ans = input(query+" [y/n] ") 211 | except KeyboardInterrupt: 212 | input_yn(query) 213 | 214 | while ans not in ["y", "n"]: 215 | ans = input("'y' or 'n' please: ") 216 | 217 | return ans == "y" 218 | 219 | def parse_date(file): 220 | ''' 221 | parses date out of pre-validated filename 222 | 223 | * assumes a filename of YYYYMMDD.txt 224 | * returns a list: 225 | [0] 'YYYY' 226 | [1] 'MM' 227 | [2] 'DD' 228 | ''' 229 | 230 | rawdate = os.path.splitext(os.path.basename(file))[0] 231 | 232 | date = [rawdate[0:4], rawdate[4:6], rawdate[6:]] 233 | 234 | return date 235 | 236 | --------------------------------------------------------------------------------