Codebase list oysttyer / 8dc1875
Import Upstream version 2.9.1 Thorsten Alteholz 5 years ago
7 changed file(s) with 9738 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 language: "perl"
1 perl:
2 - "5.22"
3 - "5.20"
4 - "5.18"
5 - "5.16"
6 - "5.14"
7 install: true
8 script: "perl -c oysttyer.pl"
0 #CHANGELOG
1
2 ## Version 2.9.1
3
4 ### Issues resolved
5
6 - `synch` caused the program to hang ([#123](https://github.com/oysttyer/oysttyer/issues/123))
7 - The `/web` command did not work for tweets with a code beginning with "d". ([#122](https://github.com/oysttyer/oysttyer/issues/122))
8
9 ##Changes in Version 2.9:
10
11 - Set the value of %URL% to the result of `/short` ([#112](https://github.com/oysttyer/oysttyer/issues112))
12 - Add support for Perl 5.30+ by replacing `sysread()` with `read()` ([#115](https://github.com/oysttyer/oysttyer/issues/115))
13 - Prevent crashes with `/list +N` ([#114](https://github.com/oysttyer/oysttyer/issues/114))
14 - Do not display notifcations when you like a tweet that was retweeted into your timeline ([#98](https://github.com/oysttyer/oysttyer/issues/98))
15 - Correctly counts the length of t.co links ([#116](https://github.com/oysttyer/oysttyer/issues/116))
16 - Add support for selecting `highest` or `lowest` resolution for videos ([#77](https://github.com/oysttyer/oysttyer/issues/77))
17 - Creates a lock file when running in daemon mode ([#106](https://github.com/oysttyer/oysttyer/pull/106))
18 - Open a tweet in a web browser with the `/web` command ([#101](https://github.com/oysttyer/oysttyer/issues/101))
19
20 ### Known issues
21
22 - `synch` causes the program to hang ([#123](https://github.com/oysttyer/oysttyer/issues/123))
23
24 ##Changes in Version 2.8.1:
25
26 - Fixes a bug introduced in 2.8.0 that caused multi-image tweets to only display the first image. ([#95](https://github.com/oysttyer/oysttyer/issues/95))
27 - Fixes a bug introduced in 2.8.0 that caused videos to display with an inconsistent type. Videos will now be displayed in mp4 if that format is available and fall back to m3u8. ([# 93](https://github.com/oysttyer/oysttyer/issues/93))
28
29 ##Changes in Version 2.8.0:
30
31 - Add -extended option to support extended tweets (thanks, myshkin!)
32 - Add -origimages option to request original-sized images (thanks, Wyatts)
33 - Re-add mp4 URL replacement since that is still used for GIFs.
34
35 ##Changes in Version 2.7.2:
36
37 - Nothing, just constant goddamn interruptions mean I make silly mistakes with versioning.
38
39 ##Changes in version 2.7.1:
40
41 - Fixes /short. Needed updating to reflect latest is.gd API.
42
43 ##Changes in version 2.7.0:
44
45 - Adds /edm and /edmreply commands to use $EDITOR for replying to DMs
46 - Summary of other DM enhancements already added in point releases:
47 - Add the ability to share tweets via direct message with the `/qdm` command (Work towards of 2.7 milestone)
48 - Expand long DMs suring start-up
49 - Nicely truncate long DMs when using /dump
50 - Fix a bug where whoami comparison was not lowercased for sent dms
51
52 ##Changes in Version 2.6.4:
53
54 - Add --http1.1 flag to curl to suit versions after 7.47.0. Earliest supported curl is now 7.33.0
55
56 ##Changes in version 2.6.3:
57
58 - Nothing over 2.6.2. I just can't tag things properly
59
60 ##Changes in version 2.6.2:
61
62 - Correct year on startup screen
63 - Adds very rudimentary CI syntax checks
64 - Expand long DMs suring start-up
65 - Nicely truncate long DMs when using /dump
66 - Revert shebang to original
67 - Fix a bug where whoami comparison was not lowercased for sent dms
68 - Update userstream endpoint
69
70 ##Changes in version 2.6.1:
71
72 - Add the ability to share tweets via direct message with the `/qdm` command (Work towards of 2.7 milestone)
73 - Use the Twitter account in the prompt instead of `oysttyer` when `showusername` is true.
74 - Add ':large' to Twitter image URLs when `largeimages` is true.
75 - Add a space between tweets when `doublespace` is true.
76 - Fixed an issue where retweeted tweets displayed the wrong timestamp.
77 - Fixed an issue where tco were not destroyed in threads
78 - Display link to video file instead of link to video thumbnail in tweets
79 - Display video files in `/entities`
80 - Bring `/entities` back into Twitter TOS compliance and make it only open tco links (I.e. make it behave worse. Sorry)
81 - Add tab expansion for like and retweet (missed from 2.5.1)
82
83 ##Changes in version 2.6.0:
84
85 - Finishes up newline support
86 - Correctly counts characters for strings with newlines that are being sent. I.e. `\n` counts as one character.
87 - Summary of newline behaviour already implemented:
88 - Any `\n` in a tweet will be sent as a newline
89 - To send a literal `\` followed by a `n` you have to escape and type `\\n`.
90 - The `-newline` command line argument/option can now be optionally set to `-newline=replace` as well as on/off (`1` or `0`)
91 - If newline is set to replace then you can specify what oysttyer uses for display of newlines using `-replacement_newline` and `-replacement_carriagereturn` or use the default replacement characters
92 - Note: If using `-newline=replace` there is currently no way for oysttyer to differentiate between actual newlines and literal `\`s followed by literal `n`s and both will get replaced.
93
94 ##Changes in version 2.5.2:
95
96 - Add /mute /unmute functionality
97
98 ##Changes in version 2.5.1:
99
100 - favorites changed to likes (Twitter made everyone do it!)
101 - Quick, perhaps temporary, fix to allow users to specify their own oauthkey and oauthsecret in their .oysttyerrc to work around the current muzzling issues
102 - fix /vre to not break threading
103 - Allow custom newline replacement characters
104
105 ##Changes in version 2.5.0:
106
107 - Rename to oysttyer
108 - Change API key, etc
109 - No new features or function changes since 2.4.2, just renaming
110
111 ##Changes in version 2.4.2:
112
113 - Start implementing improved newline behaviour, towards 2.6.0 milestone.
114 - Can now send newlines with literal "\" followed by literal "n".
115 - Allow sending longer DMs (2.7.0 milestone)
116 - Remove own username when replying to self.
117
118 ##Changes in version 2.4.1:
119
120 - Fix "display" of multiple images in tweets so extensions can pick them up. Specifically so deshortify can underline them.
121
122 ##Changes in version 2.4.0:
123
124 - Version checking url changed to this repo on Github so I don't have to spam Twitter everytime I've updated
125
126 ##Changes in version 2.3.1:
127
128 - Update built-in help to reflect that /rt can be used to create quote tweets
129
130 ##Changes in version 2.3.0:
131
132 - "Displays" multiple images if a tweet includes them; the urls of the additional images are appended to the tweet text
133 - /entities command now lists out both entities and extended\_entities.
134 - /url and /open open links from extended\_entities as well as entities. Duplicated links aren't opened.
135 - Note: Due to perceived compliance with Twitter's Terms of Service the t.co links are opened for multiple images which unfortunately means that just one link gets opened no matter how many images are attached. Whether or not this is strictly required will be investigated and if we can open the links directly to the image files TTYtter will be updated to suit.
136
137 ##Changes in version 2.2.4:
138
139 - No changes, I just forget to change version in ttytter.pl. Constantly distracted.
140
141 ##Changes in version 2.2.3:
142
143 - Fix empty geo coordinates for quoted tweets
144 - Badge quoted tweets themselves as well as the parent
145
146 ##Changes in version 2.2.2:
147
148 - Destroy tco in quoted tweets that are nested in new RTs. Missed this under 2.2.1
149
150 ##Changes in version 2.2.1:
151
152 - Destroy tco in quoted tweets. Missed this under 2.2.0
153
154 ##Changes in version 2.2.0:
155
156 This unofficial version is my first attempt at maintaining TTYtter and introduces quoted tweet support.
157
158 - Create quoted tweets. Simply append text to the /retweet command. You are allowed 116 chars and TTYtter should check and warn if you go over.
159 - Displays quoted tweets automatically. Parent tweets are identified with a quote mark (") whereas standard retweets keep the percentage symbol (%). The quoted tweet will be displayed immediately below the parent tweet as a fully functioning tweet (i.e. it gets a menu code). Straight retweets of quoted tweets also display the quoted tweets. However, like the Twitter website, no further recursion of quoted tweets are made, i.e. a quoted, quoted tweet isn't displayed. For that use the /thread command.
160 - filterrts extended to also apply to quoted tweets, etc.
161 - /thread command extended to support quoted tweets and recurse through for the same amount as it does for replies, etc.
162 - version checking of TTYtter disabled since this is all unofficial.
163
164 ##Changes in version 2.1.0:
165
166 This version of TTYtter requires Twitter API 1.1. Twitter-alike services not compliant with API 1.1 will not work with this or any future version of TTYtter. If you need API 1.0 support, you must use 2.0.4 for as long as that support is available.
167
168 - Full compliance with Twitter API 1.1, including TOS limitations and rate limits.
169 - TTYtter now deshortens t.co links transparently for tweets and events, and uses t.co length computations when determining the length of a tweet. This feature can be disabled with -notco. If you are using Term::ReadLine:TTYtter 1.4 or higher, then this will also work in readline mode.
170 - Commands that accept menu codes can now also accept tweet or DM IDs, perfect for all you command-line jockeys.
171 - New /replyall command (thanks @FunnelFiasco).
172 - New /del command.
173 - User filtering (with new -filter\* options).
174 - Better description of the full range of streaming events (thanks @RealIvanSanchez).
175 - /push now works with non-Boolean options, simply pushing them to the stack (it still sets Booleans to true when pushed).
176 - The background will kill itself off correctly if the foreground process IPC connection is severed (i.e., the console died), preventing situations where the background would panic or peg the CPU in an endless loop.
177 - Geolocation now looks at and processes place ID, country code, name and place type, and tweets with a place ID will also be considered to have geolocation information (thanks @RealIvanSanchez).
178 - Using -twarg generates a warning. As previously warned, it will be removed in 3.0.
179 - -anonymous now requires -apibase, as a Twitter API 1.1 requirement.
180 - All bug fixes from 2.0.4.
181
182 ##Changes in version 2.0.4 (bug fixes and critical improvements only; these fixes are also in 2.1.0):
183
184 2.0.x will be the last branch of TTYtter to support Twitter API 1.0. When the 1.0 API is shut down, all previous versions of TTYtter will fail to work and you must upgrade to 2.1.x.
185
186 - You can now correctly /push booleans that were originally false.
187 - /eval now correctly emits its answer to $streamout so that -runcommand works.
188 - /vcheck on T::RL::T now correctly reports the currently installed version rather than the server's version when the installed version is the same or newer.
189 - Error messages from Twitter are properly processed again, so that commands that really fail won't unexpectedly appear to succeed.
190 - Hangs or spurious errors in -daemon mode are now reduced.
191 - The list\_created event is now properly recognized in streaming mode.
192 - /entities on a retweet now properly refers back to the retweet.
193
194 ##Changes in version 2.0.3:
195
196 - Various and sundry Unicode whitespace characters are now canonicalized into regular whitespace, which improves URL recognition and editing. This occurs whether -seven is on or not. (thanks @Ryuutei for the report)
197 - You can now turn the ability of a user to send NewRTs on and off with /rtson and /rtsoff, respectively, as a down payment on full filtering in 2.1. Note that this does not currently filter NewRTs out of the stream; this is a Twitter bug.
198 - The user\_update event is now properly recognized in streaming mode.
199
200 ##Changes in version 2.0.2:
201
202 - /trends now accepts WOEID (either set with /set woeid or as an argument). If none is given, global trends are used instead (WOEID 1). The old $trendurl will be removed in 2.1, since this makes it superfluous. Speak now if this affects you.
203 - If you have a location set with /set lat and /set long, the new /woeids command will give you the top 10 locations Twitter supports that match it. You can then feed this to /trends, or set it yourself.
204 - Repairs another race condition where posting before signal handlers were ready could crash TTYtter (thanks @RealIvanSanchez for the report).
205 - The /entities command is now smarter about media URLs.
206 - The exponential backoff is now correctly implemented for reconnecting to the streaming API. If a connection fails, the timeout will automatically extend to a maximum of 60 seconds between attempts. In the meantime, TTYtter will transparently fall back on the REST API.
207 - Extension load failure messages are now more helpful (thanks @vlb for the patch).
208 - Prompts were supposed to be case-insensitive, and now they are (thanks @FunnelFiasco for the patch).
209 - /whois (and /wagain) and /trends now correctly emit to $streamout so that -runcommand works.
210
211 ##Changes in version 2.0.1:
212
213 - Expands UTF-8 support to understand UTF-16 surrogate pairs from supra-BMP code points, fixing the Malformed UTF-8 errors generated by Perl for certain characters.
214 - A race condition where TTYtter could accidentally kill the foreground in streaming mode is fixed (thanks @WofFS for the report).
215 - -backload=0 now properly populates $last\_id, even if no tweets are received after the initial "fetch," eliminating an issue with spuriously grabbing old tweets (thanks @Duncan_Rowland for the report).
216
217 ##Changes in version 2.0.0:
218
219 - Introduces Streaming API support (opt-in) on systems satisfying prerequisites, using Twitter User Streams.
220 - Reworked event and select() handling for better reliability on a wider array of operating systems.
221 - List methods are now overhauled to remove deprecated endpoints. As a consequence, if your extension relied on the undocumented function &liurltourl, you must update it, as that function is no longer used for the current REST API revision.
222 - The old public\_timeline endpoint is deprecated by Twitter and has been removed. Anonymous users will only see tracked terms, if any.
223 - /tron, /troff and /track are now case-insensitive, matching the Search API (thanks @\_sequoia for the report).
224 - $conclude is now properly called after /again.
225 - Now that Twitter properly supports retweet counts greater than 100, so does TTYtter.
226 - Underlining of user names properly ignores hyphens.
227 - TTYtter will no longer run with Term::ReadLine::TTYtter versions prior to 1.3.
228 - Various cosmetic fixes.
229 - API changes: $eventhandle for receiving non-tweet/DM events from the Streaming API.
230 - -twarg, a very old holdover of the old single-extension API in TTYtter 0.x, is now deprecated; it does not scale in the multi-module environment. It will be removed in 3.0. Migrate your extensions now.
231 - -oldstatus, which was deprecated in 1.1.x, is now removed. If you are relying on the old behaviour, you must use 1.2.5.
232 - xAuth (not XAuth), which was deprecated in 1.2.x, is now removed. If you are relying on the old little-xAuth authentication system, you must use 1.2.5.
233
234 ##Changes in version 1.2.5:
235
236 - Fixes for signals on Linux 3.x kernels, which includes newer releases of Debian and Ubuntu. If you are using readline mode, this requires Term::ReadLine::TTYtter 1.3, which is released simultaneously and has the following fixes:
237 - Matching fixes for signals on Linux 3.x kernels.
238 - CTRL-D as the first character on a line is now correctly seen as EOF, matching the non-readline version.
239 - URL-sniffing logic now uses the earlier, more conservative algorithm to eliminate spurious characters (thanks @fukr for the report).
240
241 ##Changes in version 1.2.4:
242
243 - The -status=- patch in 1.2.3 broke passing statuses on the command line (that'll teach me to proof patches better). Fixed; thanks @dogsbodyorg for the spot.
244
245 ##Changes in version 1.2.3:
246
247 - Signals restructured to allow $SIG or POSIX.pm-based signalling. The latter is preferred for Perl 5.14+; the former is preferred for for 5.8.6+, 5.10 or 5.12, and is the only supported method for unsupported Perls (viz., 5.8.5 and earlier). This should eliminate the need to manually set PERL\_SIGNALS to unsafe for Perl 5.14+, assuming that you have POSIX.pm. You can force TTYtter to use POSIX.pm signals with -signals\_use\_posix, but it's better to let it choose which method it prefers.
248 - Repairs to -retoke, which should once again work with dev.twitter.com.
249 - Tweak for multi-line -status=- (thanks @paulgrav for the patch).
250 - The old, undocumented debugging option -freezebug was obsolete as of 1.2, and now is completely removed.
251
252 ##Changes in version 1.2.2:
253
254 - New /entities command extracts t.co links from tweets and DMs so you can see where they point.
255 - Fixed /trends to use new URL (thanks @Donearm for the report).
256 - Fixed /trends not to double-double-quote strings when they are already double-quoted. Because that would double-quote them double, you dig?
257
258 ##Changes in version 1.2.1:
259
260 - Changes to Search API optimizer to accommodate other entities. (A more complete solution eliminating the optimizer entirely is planned for 2.0.)
261 - RAS syndrome corrected in keyfile generator (with thanks to the supremely pedantic @FunnelFiasco ;).
262
263 ##Changes in version 1.2.0:
264
265 - Perl 5.8.6 is now the minimum tested version (but see this note on 5.005 and 5.6).
266 - xAuth support is now deprecated and will be removed in 2.0. Speak now if this will affect you.
267 - New list support, including building, editing and disposing of lists directly from the client, and mixing lists into your timeline dynamically. You can even turn off your regular timeline and just use a list as your timeline to see only a subset of users. Don't worry, your favourite grouping extensions still work too.
268 - Many commands can now take an optional +count, allowing limited pagination.
269 - NewRTs are now the default for /retweet, and the NewRT interface is now complete with retweet counts in tweets, NewRT marking for tweets and /rtsof. /thread also tracks NewRT linkages (thanks @augmentedfourth for the suggestion), and you can /delete them like any other tweet. Appending to a retweet or /oretweet uses the old RT format, or you can say -nonewrts.
270 - New users now authorize with standard OAuth, eliminating our dependence on the old Twitter key clone system. Users who already have cloned keys don't need to do anything; they will still work. New users should use OAuth. 1.2's -retoke credentials generator also uses OAuth.
271 - A "pastebrake" reduces spurious tweets caused by accidentally pasting into the TTYtter window.
272 - The promised /dmsent command is now implemented.
273 - TTYtter's fetch algorithm has been changed to a "sliding window" system to try harder to get tweets posted out of order, as well as cope with high frequency search keywords.
274 - You can now specify a custom path to your notify tool for both Growl and notify-send using -notify\_tool\_path=....
275 - You can use %%\* if you misfired an argument. For example, /re e5 right on bro followed by /re f4 %%\* becomes /re f4 right on bro
276 - The /vcheck command will now automatically populate %URL% with the appropriate URL, so now you can just /open it (thanks @dirtyHippy for the idea).
277 - -statusurl lets you shorten and append a URL to a -status (thanks @microlifter for the patch).
278 - .ttytterrc is treated as UTF-8 by default (thanks @kseistrup for the report; wontfixed for 1.1 for compatibility reasons).
279 - -backload=0 shouldn't load anything, and now it doesn't (thanks @jfriedl for the report; wontfixed for 1.1 for compatibility reasons).
280 - -lib and -olib are now completely removed.
281 - API changes: $userhandle for displaying user objects, and new library functions &postjson &getbackgroundkey &sendbackgroundkey.
282 - All bug fixes from 1.1.11 and 1.1.12.
283
284 ##Changes in version 1.1.12 (bug fixes and critical improvements only; these fixes are also in 1.2.0):
285
286 - Patches for Perl 5.14 (thanks @rkfb for the report).
287 - Keyfiles can now be regenerated if they are corrupted or need to be updated with -retoke.
288 - /doesfollow should give true or false with -runcommand (thanks @kaleidoscopique for the report). Similarly, /short should also work, emitting the URL (thanks @microlifter for that report).
289 - Properly understands a new Twitter ad-hoc error format, which repairs some operations that would unexpectedly appear to succeed but didn't actually (thanks @augmentedfourth for the report).
290 - -readline autocomplete command list now up-to-date.
291
292 ##Changes in version 1.1.11 (bug fixes and critical improvements only; these fixes are also in 1.2.0):
293
294 - Fixed a bug where TTYtter crashes ungracefully if OAuth credentials fail.
295 - Fixed regex in command processor that interpreted all commands starting with /p as /print.
296 - -notimeline is now properly recognized by /set as a boolean.
297 - One last issue related to URL shortening.
298
299 ##Changes in version 1.1.10:
300
301 - Code adjustments to avoid double-decoding UTF-8 sequences internally (thanks @cristiangauma for the fix).
302 - Fixed crash in readline autocompletion when metacharacters were present (thanks @stormdragon2976 for the report).
303 - Optimized readline statistics are now case-insensitive so that weighting is more correct.
304 - Corrected flaw with -verify where prompts went to the wrong filehandle.
305 - Keyword terms in /trends are now quoted for search (thanks @WofFS for the report).
306 - /short more securely encodes its input so that certain URLs will not be shortened incorrectly (thanks @alexfalkenberg for the report).
307 - Custodial code cleanup pre-1.2.
308
309 ##Changes in version 1.1.9:
310
311 - Signals now should operate correctly on Solaris and other systems using SIGXCPU/XFSZ (thanks @jgeorgi for the report).
312 - StatusNet and Identi.ca support is restored, using a shim that dynamically works up the missing stringified-int fields 1.1.8+ requires.
313 - -linelength lets you set an arbitrary linelength for Twitter-alike APIs not limited to 140 characters (the default is, of course, 140).
314 - -notifyquiet turns off the test notify sent by your chosen notification driver.
315 - -daemon mode is no longer limited by the need to assign menu codes, allowing it to accept ridiculously large data slurps.
316
317 ##Changes related to Term::ReadLine::TTYtter version 1.2:
318
319 - T::RL::T now keeps up with changing terminal sizes, which should reduce overpaint (thanks @WofFS for the fully functioning patch).
320 - Pressing DEL at position 0 no longer causes the app to exit. This was, unbelievably, an intentional feature of T::RL::Perl.
321
322 ##Changes in version 1.1.8:
323
324 - Emergency fix for signature errors (due to status IDs now overflowing the base ID fields). This may cause TTYtter to be incompatible with some Twitter-alike APIs; I can't do anything about that until they start supporting the \*\_str versions.
325 - Smoother fetching from the Search API.
326
327 ##Changes in version 1.1.7:
328
329 - -daemon mode works again.
330 - New-format Twitter error messages are automatically unwrapped.
331 - Changes related to Term::ReadLine::TTYtter (version 1.1 is required for this support):
332 - Perl 5.6 is now required explicitly to use T::RL::T. (You can still use 5.005 without -readline, but see the support note above).
333 - Most UTF-8 characters should now be properly accepted, and more keyboard layouts work properly on more operating systems.
334 - Prompts that are not transmitted to Twitter do not have the character counter, such as Y/N confirmation prompts and so on.
335 - The character counter can be disabled completely with -nocounter (as an option to ttytter) for screen readers.
336 - The prompt now defaults to ANSI off, unless you pass ttytter the -ansi option. This also allows you to turn ANSI on and off and the prompt will follow. (If you use T::RL::T 1.1 with TTYtter 1.1.6, you will notice that the prompt is no longer highlighted because 1.1.6 doesn't know how to synchronize ANSI state.)
337 - /unset now sets non-Boolean options now to undef so that it will "do the right thing."
338 - I swear, /troff no longer strips quotes off quoted terms. If it does, give me your exact track list and the keyword you used. I swear by all that is holy I fixed it this time!
339 - API tweak: &wraptime, which was "optimized" out in 1.1.6, has been restored as a stub in 1.1.7 for compatibility.
340
341 ##Changes in version 1.1.6:
342
343 - 1.1.6 is a very large systems update, touching quite a bit of low-level code. In particular, this version requires full POSIX signals to function at all, whereas previous versions only needed them in certain circumstances: your system must support either or both of SIGUSR1/2 or SIGPWR/SYS (i.e., signals 30 and 31), which are used as software interrupt signals between the foreground and background processes, or TTYtter will crash or hang. This has been verified to work on all the supported systems above.
344 - If your TTYtter abruptly quits when you type commands, your system does not support these signals correctly. Send me a report so that I can investigate a workaround.
345 - Support for repaintable readline prompts, when combined with a supporting driver such as Term::ReadLine::TTYtter. T::RL::T is custom-designed for this purpose, including dynamic repainting, history synchronization, background signaling, improvements to UTF-8 support and even a character counter. You can add support to your favourite readline driver with some extra stub functions. If you use T::RL::T as your readline driver (which is now the default for -readline if it is installed), /vcheck and -vcheck check its version too. This driver is a beta. It is still in development. Expect bugs.
346 - Location support with -location, -lat and -long. Your account must be geo-enabled, which cannot be done from TTYtter; you must do it from the Twitter web interface. You can then set a (default) location with -lat/-long, and use -location to toggle if/when to send it.
347 - /block and /unblock for those users you hate, like @dickc.
348 - The foreground now sends squelch signals to the background when a command is running, which should reduce command output stepping on background updates.
349 - -searchhits specifies how many search results to grab from the Search API, both with /search (thanks @jdvalentine) and tracked results.
350 - /set [boolean] can now be used to set a Boolean option to 1, like /set ansi. Similarly, /unset can now set an option to zero (or literal string "0"). These commands are mostly intended for booleans and may not work right with other options.
351 - -status can now be passed a line of text over standard input if you use -status=- (that's dash "status" equals dash), which is useful for scripts that can't trust their input but really want to use -hold (speaking of, a bug with -hold holding for an incorrect duration should now be fixed too). If your script can't cope with this and absolutely needs the old behaviour, -oldstatus is available as a deprecated stopgap to use the old -status behaviour, but may disappear in future versions.
352 - Faster UTF-8 processing.
353 - Growl notifications on Mac OS X are now asynchronous, which significantly improves their processing speed.
354 - Background event loop rewritten to drive select() in a more compatible fashion, which should eliminate random freezes (-freezebug is still in 1.1.6 for purposes of debugging, just in case).
355 - TTYtter now tells you what readline driver it is using, if any. You can set the PERL\_RL environment variable to override this (such as Gnu, Perl, TTYtter or Stub).
356 - All prompts now use -readline when enabled.
357 - Command line options didn't always override what was in the .ttytterrc file. Fixed.
358 - Retweeting a tweet with UTF-8 characters should no longer generate a signature error.
359 - Foreground menu codes are now shown in bold to set them off from background updates.
360 - -simplestart prints an abbreviated startup banner for slower systems or more dire screen readers.
361 - JSON fetches are more compatible with arbitrary OAuth signature algorithms, which should help extension authors and /eval jockeys.
362 - The -readline TAB completion routine now includes all the supported commands (thanks again @jdvalentine).
363 - API changes: new library functions &sendnotifies and &senddmnotifies, which decouple notification from &defaulthandle and &defaultdmhandle respectively. This allows extensions to send their own notifications without relying on the default handlers (thanks @stormdragon2976 for the use case). In TTYtter 2.0, with the next major revision of the internal API, this idea will be explored much further.
364
365 ##Changes in version 1.1.5:
366
367 - Backed out select() debugging code due to way too many false positives. It can be re-enabled with -freezebug for testers.
368 - Small custodial changes in progress.
369
370 ##Changes in version 1.1.4:
371
372 - You can now ask for additional tweets to backfill your timeline with -backload=[number]. Careful with this option: Twitter can ignore it, and often does, and loading large amounts of data can dramatically slow TTYtter down. This is a down payment on pagination, to come in the very near future.
373 - You can now specify multiple arguments to -notifytype, such as =growl,libnotify. You will probably need an extension for your particular notification scheme. (suggested by @stormdragon2976)
374 - Correctly recognizes the StatusNet "fail whale" (thanks @seppo0010 and @yrvn).
375 - Adjusted user-agent timeouts for iffier links.
376 - Rescue code for buggy user-agents that ignore timeouts.
377 - More HTML entities are deciphered in both regular and -seven modes.
378 - A platform-inless dependent change of the default keyfile umask for better security (thanks @herrold).
379 - Gopher URLs are now forwarded to the Floodgap Gopher proxy, since Firefox 4 is dropping Gopher support, unless -urlopen uses lynx as its user agent, and /short on gopher URLs adds the proxy on to get an HTTP URL. (Hey, this is a text client. I have to support gopherspace.)
380
381 ##Changes in version 1.1.3:
382
383 - The JSON parser incorrectly rejects some null strings, which can interfere with logging into OAuth. Fixed. (thanks @alfredhallmert)
384 - Metacharacters in URLs are now (should be) correctly rejected when fed to the TAB-shortener in -readline. (thanks @johndalton)
385 - Replies now take priority always over search results with -mentions.
386 - Exception messages are now timestamped also if -timestamp is on. (suggested by @colindean)
387 - /cls command to clear the screen. (suggested by @schapendonk)
388 - Spurious failure with perl -c in 5.005 worked around.
389 - Corrections to messages and the introductory blurb.
390
391 ##Changes in version 1.1.2:
392
393 - -status with UTF-8 characters now works correctly again from the command line (as long as your locale is set correctly, of course). (thanks @jlm314)
394 - $shutdown now correctly fires even if a child process was not launched.
395
396 ##Changes in version 1.1.1:
397
398 - Corrected (fingers crossed) OAuth signature bugs and UTF-8 problems. Tested on Ubuntu 10.04, Mac OS X 10.6/10.5/10.4 (PPC and x86), AIX and NetBSD 5 with 5.005 through 5.10.1, so if it doesn't work for you, I'll just find a quiet corner and shoot myself. Yes, it's actually shorter than 1.1.0 due to some efficiencies that were possible. (thanks @j4mie, @dariuus, @seppo0010 and many others for data points)
399 - When looking for tools, TTYtter will now check your path first before its built-in locations. (thanks @seppo0010)
400 - Better handling for impoverished environments where $HOME may not be defined.
401 - New mention in Guinness Book of World Records for quickest replacement of a version of TTYtter. It's in the back somewhere, near record number of hours watching Monty Python while singing from the Hungarian Bongosok.
402
403 ##Changes in version 1.1.0 (this version is an updated form of the public beta, released as is due to the switchover; expect minor bugs, which will be rectified in 1.1.1):
404
405 - Official support for OAuth, which is now the default method of authentication. OAuth requires cURL -- Lynx will not work. Basic Auth is still supported for users of StatusNet and Identi.ca, and still works with Lynx, but you must ask for it with -authtype=basic. After 16 August 2010, only TTYtter 1.1.0 and later will be able to access Twitter due to the Basic Auth shutdown. No earlier version of TTYtter will work! Read the main page for how to get your credentials converted to OAuth. You only have to do this once per account.
406 - Foreground menu codes now roll continuously and wrap around instead of resetting with every foreground command (except for /thread, which still uses zz0 to zz9). This is the completion of the menu code change first introduced in 1.0.0.
407 - Support for automatically fetching replies with -mentions, even from users you do not follow.
408 - /deletelast deletes the most recent tweet you made, if you don't like using proper safety nets like -verify or -slowpost.
409 - /doesfollow command (part of 1.0.4, but originated with the aborted 1.1.0 public beta), telling you if a user follows another or if a user follows you.
410 - For users requiring -seven, certain single character entities will now be translated from UTF-8 to the nearest ISO-8859-1 equivalent (part of 1.0.4, but originated with the 1.1.0 public beta). This table will expand in the future.
411 - Various API changes: -lib and -olib are now removed; new library functions; $getpassword and $shutdown (suggested by @colindean).
412 - All bug fixes from 1.0.3 and 1.0.4.
413
414 ##Changes in version 1.0.4 (these fixes are also in 1.1.0):
415
416 - Search API URLs corrected to Twitter-specified URLs.
417 - NewRTs now appear in user timelines and mentions, thanks to new improvements in the Twitter API.
418 - Ported /doesfollow and the improved UTF-8 entity translation for -seven from the forthcoming revised 1.1 beta.
419
420 ##Changes in version 1.0.3 (bug fixes and critical improvements only; these fixes are also in 1.1.0):
421
422 - Search API URLs now transitioned to the api.twitter.com endpoint, as the old ones will be eventually shut down.
423 - When terminating TTYtter correctly exits with the right error status now (thanks @jlm314).
424 - Reply username matching is now a bit less greedy.
425 - Spaces are trimmed off URLs in /whois.
426
427 ##Changes in version 1.0.2:
428
429 - Missed one of the bleeding colour bugs into the -readline prompt that was supposed to be fixed in 1.0.1. Fixed for sure this time. (thanks @tjh)
430 - Updated API URLs.
431 - Search API support streams more reliably and is compatible with future changes to the Search API search method.
432
433 ##Changes in version 1.0.1:
434
435 - Fixed JSON parser to avoid bailout with certain large GeoAPI coordinates. (thanks @pssdbt)
436 - TTYtter now counts in UTF-8 characters, not bytes, now that I have confirmation of full support in the Twitter API. 140 character tweets and DMs are now fully supported, and also work with -autosplit.
437 - Multi-module loader properly insulates non-fatal errors from the extension. This should improve compatibility. (thanks @colindean)
438 - Error messages won't foul prompt colour in -readline mode anymore (thanks @wireghoul).
439 - -synch mode updates are only triggered now for successful posting, not on overlength tweets, etc.
440
441 ##Changes in version 1.0.0:
442
443 - Source code reorganized and in some cases completely rewritten.
444 - Multi-module system for the TTYtter API allows you to install and run multiple extensions simultaneously (if compatible), adding the new -exts option.
445 - Speaking of, massive changes to the TTYtter API. Extension authors should re-read the API documentation for compatibility notes. While many extensions will work with no or minimal changes, some may need to be updated.
446 - The old -lib and -olib options are now deprecated, and will be removed in the 1.1 releases.
447 - Synchronicity mode synchronizes updates with your keyboard activity (-synch), but has a price to pay. Mostly intended for input methods that are unhappy with background updates.
448 - -runcommand option for simple command-line queries.
449 - -hold is no longer infinite when used with -script.
450 - Tweet code temporary menus now occupy a three character menu code that always starts with z (so now /thread generates zz0 through zz9). This is to accommodate future menus that may be more than 20 entries.
451 - Initial support for the Retweet API and newRTs. NewRTs now appear in your timeline by default, are properly unwrapped so they are not truncated, and are canonicized to appear just like RTs used to. Retweets-of-me are displayed using the new /rtsofme command (/rtom). Note that because the API doesn't give you information about who retweeted you, neither does this command. Twitter acknowledges this deficiency and it will be supported in a later TTYtter when they fix it. If you want to disable NewRTs (such as for StatusNet, etc.), use -nonewrts. RTs made with /rt and friends are still the manual variety.
452 - /follow and /leave now handle following and leaving users (no more FOLLOW and LEAVE even though they are still supported).
453 - /dm who what replaces D who what (although the latter will still work), giving you your 140 characters all back, and is properly supported by -autosplit, -slowpost and -verify. /replying to a DM now internally uses /dm.
454 - /dump now supports the Geolocation API and Retweet API, giving you location information for tweets that encode it, plus the retweet metadata. More information is also in the tweet cache for later.
455 - A new versioning system recognizes when you are using a beta and checks the internal build number.
456 - Special logic to detect the Fail Whale for more bulletproof posting and more useful error messages.
457 - /again and /whois get confused by numeric Twitter user IDs (and treat them as user numbers). Patched to fix this so that numeric IDs are seen as true IDs. Although this also affects 0.9, it requires making an incompatible change, so it will not be fixed in that version.
458 - If -rc gives an absolute path, use that. (thanks @FunnelFiasco)
459 - All bug fixes from 0.9.10, 0.9.11 and 0.9.12.
460
461 ##Changes in version 0.9.12 (bug fixes and critical improvements only; these fixes are also in 1.0.0):
462
463 - If you /troff on a keyword set that has quoted phrases, the quotes get lost off all of them. Fixed.
464 - Restoring from /set tquery 0 also fouls up quoted search terms. Fixed.
465 - Setting $tquery in an extension's initialization does not override $track. Fixed. (thanks @colindean)
466
467 ##Changes in version 0.9.11 (bug fixes and critical improvements only; these fixes are also in 1.0.0):
468
469 - Warn the user if a notification framework was selected but no notifies were requested. This might be useful for an extension to dynamically control, so it is not a fatal error.
470 - Another try at properly handling GeoAPI information (thanks @chfrank\_cgn).
471 - Author breaks 50,000 tweets. A loud sobbing noise can be heard from Twitter corporate headquarters throughout most of the Bay Area.
472
473 ##Changes in version 0.9.10 (bug fixes and critical improvements only; these fixes are also in 1.0.0):
474
475 - If the foreground process exits abnormally, it should still clean up the background process.
476 - -script and -verbose should work together better (a more effective fix is in the 1.0.0 beta).
477 - The -slowpost prompt lagged the signal switch ever so slightly, meaning you could hit ^C and kill the process even when it told you it was okay. The prompt is now delayed until after the signal handler change.
478 - -notifytype=0 should work fully now.
479 - -script and -status now correctly ignore -slowpost and -verify.
480 - /vreply format tweaked slightly.
481
482 ##Changes in version 0.9.9 (bug fixes and critical improvements only):
483
484 - Tweets with geolocation information no longer cause the JSON parser to panic.
485 - If -autosplit=word fails, fall back on =char instead of completely destroying the tweet.
486 - /vre no longer threads the reply, as API changes have caused threaded tweets to be only visible to the one replied to.
487 - The planned conversion of 140 bytes to 140 characters as the tweet length could not be implemented in this version as the Twitter API does not correctly accept them yet.
488
489 ##Changes in version 0.9.8 (bug fixes and critical improvements only):
490
491 - Identica fixes: base URL returned to friends\_timeline; fixed the "null list" warnings Identica users were getting; updated JSON parser to understand the new Identica fields.
492 - You can now say -notifytype=0 on the command line to disable a notifytype in your .ttytterrc.
493 - -hold can potentially loop forever even if you don't want it to. -hold=1 or -hold by itself keeps the old behaviour, but specifying an argument greater than 1 causes the script to stop after that many unsuccessful tries. In 1.0.0, this will be changed again.
494 - Auto-ratelimiting changed to use 50% instead of 60%. This slightly diminishes responsiveness, but seems to help people who were getting beaten up by other client usage. You can still use -pause with an argument, of course.
495 - /[ef]rt no longer thread retweets to the source tweet. Per Twitter, this won't work right any more and actually prevents retweets from being seen (by causing them to be treated as replies).
496 - /whois and /wagain now recognize the new default images Twitter is using for accounts without avatars.
497 - -curl now works correctly again (stupid typo regression).
498 - Error codes fixed for command line tools.
499
500 ##Changes in version 0.9.7:
501
502 - 0.9 is now the stable branch and bug fixes only will occur on this branch until a stable 1.0.x becomes available, after which it will be deprecated. New development will now occur on unstable 1.0 and there will be compatibility changes. More on that when 1.0.0 is released.
503 - New notification framework with built-in support for Growl (via growlnotify) and experimental built-in support for libnotify (via modifications to notify-send; see Galago Project trac ticket #147) using -notifytype and -notifies. Expandable via the API.
504 - Revised API method for dynamic classification of tweets using the $tweettype method. (The old $choosecolour method is now deprecated and trying to call its handler will generate a fatal error. It will be completely removed in 1.0.0.)
505 - Favourites support with /favourites, /(un)fave and /frt.
506 - Tweets can be dumped and their status URLs grabbed with /dump (suggested by @augmentedfourth).
507 - /short and /url take %URL% as default, and /whois//wagain and /dump populate it, allowing you to grab URLs from status IDs or user profiles and open them or repost them (based on a suggestion from @vkoser). As a nice side effect, /url can now open arbitrary URLs as arguments.
508 - "Verified Account" support for /whois and /wagain.
509 - -slowpost mode for people needing something gentler than -verify (like me).
510 - Training-wheels mode intercepts common newbie tweets like quit and help (disabled by -slowpost and -verify; I assume that if you set those then you know what you're doing).
511 - -filter is now dynamic and can be recompiled on the fly with /set filter.
512 - /vreply forces publicly visible replies (with the de facto r @ttytter A public reply. notation).
513 - /eretweet populates %% as well to allow editing with the conventional substitution sequences (thanks @jasonwryan).
514 - To facilitate this behaviour, %-sequences are now generally interpreted at the end of a line as well, not just at the beginning.
515 - New reserved namespaces for API modules using the $store global reference in anticipation of multi-module support in 1.0.0.
516 - HTTPS URLs now accepted by /short and the TAB completer in -readline.
517 - -olib option for one-line libraries on the command line.
518 - UTF-8 characters can now be scanned for by /url, although your underlying browser may not like them (for example, Mac OS X /usr/bin/open thinks they are filenames).
519 - Default replies URL now set to mentions.json but remains the same command line option for backwards compatibility.
520 - Substitutions using %-x sequences would accept arguments that were too high and simply cut off until it couldn't anymore. This is now correctly flagged as an error.
521 - Another crash bug removed.
522 - Internal code consolidation.
523 - Better error messages for deletions, failed substitutions, etc.
524
525 ##Changes in version 0.9.6:
526
527 - Direct message selection, analogous to tweet selection, which also supports /delete, /url and /reply for a nice almost-orthogonal interface.
528 - /retweet and /eretweet, previously undocumented in 0.9.5 due to inadequate testing, are now officially supported and properly thread in-reply-to fields.
529 - Large internal change to subprocess management for easier future expansion, along with more changes to $authenticate. This internal reworking will continue up until the OAuth-based TTYtter, so people hacking on the core should beware.
530 - $choosecolour is now unstable. API programmers who are using this method should contact me, as I am planning to change the interface as part of the future notification framework.
531 - /track should not throw pagination errors on common or popular search terms. I disagree with the way Twitter has implemented this warning, but this version includes a workaround (thanks @johndalton).
532 - /ruler once again lines up properly with the prompt (thanks @vkoser, @jazzychad and others of the Brotherhood of the Ruler).
533 - Search results now are properly coloured in anonymous mode.
534 - GNU screen printed bold characters as inverse text. ANSI sequence tweaked for wider compatibility (thanks @arsatiki).
535 - Unicode code point 0x2028 needed to be seen as a newline, and subject to -newline (or not). Fixed.
536 - -noratelimit does not work when it is changed dynamically, so it is simply made a startup-option only.
537 - -filter didn't handle quote-wrapped arguments (thanks @augmentedfourth). Fixed.
538 - -wrap sometimes overindented following lines (thanks again @augmentedfourth). Fixed.
539 - Not all legal characters for URLs were accepted by /url. Fixed.
540 - /search did not call $conclude, so -filter counts got out of sync. Fixed.
541 - Author breaks 40,000 tweets. Twitter calls him on the phone to please stop and use Plurk or something.
542
543 ##Changes in version 0.9.5:
544
545 - Selection of individual tweets and threading with /thread, /reply, /delete and /url, along with @ markers on tweets that are part of a thread.
546 - -noratelimit and -notrack to disable rate limit checks and tracking keywords, respectively, on systems that don't support them (most notably Laconi.ca/Identi.ca).
547 - API addition with $choosecolour.
548 - UTF-8 characters are now allowed in tracking keywords.
549 - Faster and more reliable JSON fetch and parsing method.
550 - Expanded /help text.
551 - Bogus colour warnings when using -noansi are fixed.
552
553 ##Changes in version 0.9.4:
554
555 - Twitter Search API integration, based on initial work by @kellyterryjones, @vielmetti and @br3nda (/search, -queryurl), with hashtag integration and keyword management (/tron, /troff, /track, /#, -notimeline, -track) and trends (/trends, -trendurl), suggested by a whole bunch of people including the most esteemed @adamcurry.
556 - Customizable colours (-colour{prompt,dm,me,reply,warn}), another common request.
557 - Base API URL can now be specified for Twitter clone APIs (-apibase).
558 - Official API support for libraries driving commands, or wishing to make JSON fetches from services.
559 - Whitelisted accounts bombed with autoratelimiting. Fixed to constant value.
560 - @ highlighting in direct messages tended to bleed. Fixed.
561 - -status probably shouldn't print version check warnings. Fixed.
562 - Not every overlong prompt was getting wordwrapped. Fixed.
563
564 ##Changes in version 0.9.3:
565
566 - Automatically check that you're using the most current version, either with -vcheck at startup, or /vcheck within the client.
567 - New $authenticate API method makes it possible to store your credentials anywhere you darn well please, including nowhere. Now prompts for password when you don't specify. Based on code by @jcscoobyrs.
568 - Autosplit using the -autosplit option, suggested by @dogsbodyorg and @timtom.
569 - Correctly counts bytes in tweets, since Twitter counts in bytes, not characters (thanks @cyrixhero).
570 - Wordwrap for arbitrary screen sizes, based on a suggestion by @augmentedfourth.
571 - Verify individual tweets as you post them with -verify, along with simple Perl-expression-based filtering with -filter, based on suggestions by @cwage.
572 - Posting tweets did not show verbose information in -superverbose mode. Fixed.
573 - /setting superverbose should also set verbose. Fixed.
574
575 ##Changes in version 0.9.2:
576
577 - Status changed to 'stable' fork; previously embryonic features now either fully enabled or made default.
578 - -rc=... option allows selection from multiple .ttytterrc files, based on a suggestion by @br3nda. Corresponding -norc option allowed to, conversely, completely disable any rc file present.
579 - API additions ($addaction/&defaultaddaction).
580 - Time ranges printed for /again user (when -timestamp is not enabled).
581 - /print ntabcomp to display newly added entries during this session, based on a suggestion by @augmentedfourth.
582 - TAB completion is now case-insensitive.
583 - Expanded control character filter from 0.8.6.
584 - All bug fixes and backouts from 0.8.6.
585
586 ##Changes in version 0.8.6:
587
588 - Status changed to 'deprecated' fork.
589 - Control character filter added (backported from 0.9.x) and expanded to pre-interpret most common mistaken entries.
590 - Bug fixed with @ names framed with certain punctuation not getting highlighted.
591 - Backed out kludges for bowdlerized /whois and less efficient workaround JSON fetch.
592
593 ##Changes in version 0.9.1:
594
595 - Large rewrite of the UTF-8 handling code, with hopefully better support on as wide a range of Perls as possible.
596 - /print tabcomp to display your optimized completer string in advance, based on a suggestion by @augmentedfourth.
597 - -newline to parse \n and \r, also suggested by @augmentedfourth.
598 - CTRL-C now correctly triggers the END subroutine, reported by @augmentedfourth. Yeah, he's been busy. ;-)
599
600 ##Changes in version 0.9.0:
601
602 - Split into 'unstable' fork.
603 - Major retooling of program logic to eliminate redundant portions and streamline complex sections.
604 - Auto-ratelimit support with -pause=auto (EMBRYONIC). However, works well enough to be the default right now. If you don't want to use this, or don't trust it, you probably should be using 0.8.5.
605 - Support for Term::ReadLine::\* with -readline (EMBRYONIC), including cursor key history and TAB completion (with auto-learn), and API support with $autocompletion/&defaultautocompletion to define your own TAB completion routine.
606 - URL shortening (-shorturl and /short).
607 - Runtime changes to certain options now supported with /set and /print.
608 - Support for unusual client environments, using -leader and -noprompt, based on an idea submitted by @chfrank\_cgn.
609 - Easier SSL operations using -ssl instead of requiring changes to .ttytterrc.
610 - /again on a username reports the time of last update if you aren't using -timestamp.
611 - Friendship queries fixed.
612 - All bug fixes from 0.8.5.
613 - Author breaks 25,000 tweets. He is, truly, a nerd.
614
615 ##Changes in version 0.8.5:
616
617 - Split into 'stable' fork.
618 - Bug fixed with UTF-8 handling, even on systems and Perls that don't understand UTF-8.
619 - Bug fixed with users with no DMs.
620
621 ##Changes in version 0.8.4:
622
623 - Several temporary workarounds for glitches in the Twitter API, namely a kludge for eating invalid JSON generated by tweet deletes, disabling some fields in /whois that were pulled, and turning off friendship checks as they currently generate 500 errors. The tweaked JSON fetch is also marked as kludge. These temporary fixes will be backed out when they are fixed on Twitter's end.
624
625 ##Changes in version 0.8.3:
626
627 - Tweaked fetch routine pending eventual format of null responses (i.e., much less spurious timeout or no data messages).
628
629 ##Changes in version 0.8.2:
630
631 - Twitterer names, and @ names, are now boldface and underline respectively based on patches submitted by @smb.
632 - Expanded /whois with code for looking up friendships, and processing avatar images (-avatar, -frurl).
633 - API additions ($precommand, $prepost, $postpost).
634 - Certain HTTP status codes could cause the JSON parser to freak out. Fixed.
635 - -noansi didn't take precedence over -ansi like it was supposed to. Fixed.
636
637 ##Changes in version 0.8.1:
638
639 - $lasttwit, and origination classes for $handle, both API enhancements suggested by @emilsit.
640 - -lynx and -curl can be told to run a specific binary, useful for PATH-deficient environments or version testing.
641 - -status correctly warns for tweets over 140 characters.
642 - Speaking of which, normal tweet activity also has better warning text for oversize tweets too.
643 - Additional debugging information for failed test logins available.
644
645 ##Changes in version 0.8.0:
646
647 - Robust scripting support for simple command-line queries (/end and -script).
648 - -pause=0 is now valid.
649 - Popping words off the end of the line (%%--, etc.) works.
650 - API additions (&standardtweet, &standarddm, DUPSTDOUT).
651 - Null array references could escape from certain asynchronous commands and cause uncaught exceptions. Fixed.
652 - &prinput allegedly took arguments, but ignored them and just used $\_ like it used to. Kludged around.
653
654 ##Changes in version 0.7.1:
655
656 - Null array references could leak from the JSON parser, which would throw an uncaught Perl error. Fixed.
657 - /ruler (suggested by @jspath55).
658
659 ##Changes in version 0.7.0:
660
661 - Changes suggested and coded/adapted from code by @br3nda:
662 - ANSI colour and highlighting (and -ansi/-noansi).
663 - Timestamp support, including templates on supported installations (-timestamp).
664 - Replies support (/replies and -rurl).
665 - /again expanded to allow querying user timelines (and -uurl).
666 - API expanded with $prompt, &defaultprompt and -twarg.
667 - Anonymous mode (-anonymous).
668 - User query (/whois and /wagain, and -wurl).
669 - JSON parser upgrades to accomodate user queries.
670 - Error message reporting fixed.
671 - Proper detection of presence/absence of modules (particularly fixing problems with -seven) and streamlined BEGIN block.
672 - No need to pause with -silent.
673 - Several side effects have now been incorporated as virtues.
674 - Author breaks 10,000 tweets. What a dweeb he must be.
675
676 ##Changes in version 0.6.1:
677
678 - Improved stability in JSON validator when using Lynx as the user-agent.
679
680 ##Changes in version 0.6.0:
681
682 - Direct message support added to both interactive client and API, with -dmurl and -dmpause.
683 - -silent mode and exit statuses.
684 - Abstraction of console input processing to facilitate future expansion in both API and internal code.
685 - Recognizes new-format Twitter error messages. (Correspondingly, some API exception codes are now deprecated; see documentation.)
686 - Command abbreviations.
687 - Expanded command history support and -maxhist.
688 - Reworked error messages.
689 - Various custodial fixes and upgrades to JSON interpreter.
690
691 ##Changes in version 0.5.1:
692
693 - Patched for various entities in the new Twitter JSON release. This version will correctly handle both ampersand-escaped and standard entities and quotes.
694
695 ##Changes in version 0.5:
696
697 - Support for rate-limited API, in two ways: first, increasing default timeout to 120 seconds, and two, properly recognizing when rate-limiting has kicked in.
698 - Stability improvement in JSON validator.
699 - Additional API exception codes for the above features.
700 - select() loop tightened up to make timeline hits as minimal as possible.
701
702 ##Changes in version 0.4:
703
704 - UTF-8 now works right (most of the time). Added -seven option for backwards compatibility.
705 - First support for the TTYtter API and the -lib option.
706 - Detached mode using -daemon, allowing bot building.
707 - Tweaks to defaults.
708 - Work-around for out-of-order tweets "stuttering" or getting stuck. This is technically a Twitter bug, but this version can now ignore the anomaly.
709
710 ##Changes in version 0.3:
711
712 - Even bigger morer robuster JSON validator.
713 - Posting from the command line using -status.
714 - Can now configure update source using -update, allowing complete abstraction of TTYtter assuming the other side supports the Twitter API over JSON.
715 - -hold timeout tweaked.
716 - Messages tweaked for accuracy and semi-user-friendliness.
717
718 ##Changes in version 0.2:
719
720 - Improved detection of Twitter HTML status messages and better tolerance of partially-transmitted data (which could sometimes cause ttytter's JSON validator to freak out).
721 - Added "re-tweet" facility for ... retweeting.
722 - Added -hold option.
723 - Another hal-fassed attempt at better UTF-8 handling.
724 - Exit statuses of curl/Lynx sessions are properly reported.
725 - Proper command line precedence over default options.
0 # Oysttyer
1
2 ## Contribute
3
4 If you are a member of Oysttyer you already have commit access. It is expected you will have been running your changes locally before pushing to here, but apart from that, go for it!
5
6 ### You already have code or an idea for code you want to contribute
7
8 - Fork the project
9 - Make your additions/fixes/improvements
10 - Run you code locally and make sure your changes work and you've not broken anything
11 - Send a pull request
12
13 ### You don't already have code, nor any ideas for code, but still want to contribute code somehow:
14
15 - Have a look through issues labelled "easier" or "harder" depending on how ambitious you are feeling. I am trying to ensure all relevant issues have one of those labels.
16 - Then run through the steps as per above
17 - Feel free to link to your fork in the issue before submitting a pull request if you want a review
18 - Or just open the pull request. A pull request doesn't have to start with the finished code
19
20 ### You have ideas/bugs:
21
22 - Just open an issue with ideas for features or bugs you have found. For bugs please provide the version (commit or tag) you are using.
23
24 ## Useful tools for developers
25
26 See the oysttyer-dev repo. For now there is just one extension that can provide a full json dump of a tweet. It is very useful for inspecting the structure of a tweet per the API. Any further ideas or extensions, etc to help with developing are welcome there.
27
28 ## Testing
29
30 There are no tests! :(
31
32 I'm not necessarily the world's biggest fan of unit tests, but some tests would certainly be nice. So one great contribution would be ideas or a start on adding some kind of tests.
0
1 Floodgap Free Software License
2
3 The author of your software has chosen to distribute it under the
4 Floodgap Free Software License. Although this software is without cost,
5 it is not released under Copyleft or GPL, and there are differences
6 which you should read. Your use of this software package constitutes
7 your binding acceptance without restriction.
8
9 This software is without cost
10
11 The Floodgap Free Software License (FFSL) has one overriding mandate:
12 that software using it, or derivative works based on software that uses
13 it, must be free. By free we mean simply "free as in beer" -- you may
14 put your work into open or closed source packages as you see fit,
15 whether or not you choose to release your changes or updates publicly,
16 but you must not ask any fee for it. (There are certain exceptions for
17 for-profit use which we will discuss below.)
18
19 Definitions and terms
20
21 Author
22 The declared copyright owner of this software package.
23
24 Binary
25 A pre-compiled or pre-interpreted bytecode or machine language
26 representation of a software package not designed for further
27 modification and tied to a particular platform or architecture.
28
29 Derivative work
30 Any distribution (q.v.) that contains any modification to or
31 deviation from the official reference distribution (q.v.); or
32 any software package significantly based on or integrally
33 including the source code for its features, including but not
34 limited to supersets; subsets of a significant proportion;
35 in-place patched changes to source or binary files; linking in
36 as a library; binary-only distributions if the original package
37 included source (even if the source was not modified prior to
38 compilation); or translations to another programming language,
39 architecture or operating system environment. Derivative works
40 of packages released under this license are also considered
41 subject to this license.
42
43 However, a software package that requires this package but does
44 not include it or is not based upon it, even if it will not
45 operate without it, is not considered a derivative work. For
46 example, interpreted programs requiring an interpreter issued
47 under this license, assuming they are not distributed with any
48 portion of the interpreter, are not derivative works.
49
50 Distribution
51 A packaged release of this software, either the author's
52 original work (the "reference distribution") or a derivative
53 work based upon it.
54
55 Reference distribution
56 A packaged release of this software explicitly designated as the
57 official release, written by or on behalf of the Author with his
58 or her explicit designation as official. Only exact copies of
59 the reference distribution may be called reference
60 distributions; all other forms are derivative works.
61
62 Source code
63 The human-readable programming instructions of the package which
64 might be easily read as text and subsequently edited, but
65 requiring compilation or interpretation into binary before being
66 directly useable.
67
68 What you are permitted to do under this license
69
70 Pursuant to the remainder of the terms below,
71 * You may freely use, copy, and disseminate this software package for
72 any non-commercial purpose as well as the commercial purposes
73 permitted below.
74 * You may freely modify this package, including source code if
75 available. Your modifications need not be released, although you
76 are encouraged to do so.
77 * You may release your derivative works based upon this software in
78 purely binary (non-source) form if you choose. You are not
79 obligated to release any portion of your source code openly,
80 although you are encouraged to do so.
81 * If this package is a tool used for generation, compilation or
82 maintenance of works, including but not limited to readable
83 documents, software packages or images (for example, compilers,
84 interpreters, translators, linkers, editors, assemblers or
85 typesetters), you may freely use it for that purpose, commercial or
86 otherwise, as the works made by this package are not considered
87 subject to this license unless specified otherwise within and may
88 be distributed under any desired license and/or offered for sale or
89 rental. Any run-time library or run-time code section linked into
90 the output by a compiler or similar code-generating tool governed
91 by this license is considered to be an integral part of the output,
92 and its presence does not subject the generated work to this
93 license either. (This is, of course, assuming you are not using
94 said tools to generate a derivative work based on this package in
95 violation of the other license terms.)
96 However, if you are linking or including a separately distributed
97 library that is under this license, no matter what tool you are
98 using to do the linking or inclusion, you are then considered to be
99 making a derivative work based on that library and your work does
100 fall under this license. To avoid this, do not include the library
101 with your work (even though it needs the library to function) and
102 instead offer the library separately without cost.
103 * In addition to non-commercial use and the uses permitted above, you
104 may use this software package in any for-profit endeavour as long
105 as it does not involve the specific sale or rental of this package.
106 Some specific but by no means exhaustive examples are listed below.
107 Note that some of these situations may require additional action be
108 taken to ensure compliance.
109 + If this package or a derivative work allows you to serve data
110 or make data available to others (for example, web servers,
111 mail servers, gopher servers, etc.), you may use it to serve
112 any commercial content or in any commercial setting whether
113 you choose to charge a fee or not, as you are considered to be
114 earning income from the content you serve and/or the services
115 facilitated by your business and not from the sale of this
116 package itself. (This is, of course, assuming that you are not
117 charging a fee for sale or rental of this package or a
118 derivative work based on this package in violation of the
119 other license terms.) Similarly, any data you may acquire from
120 the use of this package is yours, and not governed by this
121 license in any way even if for-profit.
122 + If you are selling a product that includes this package or a
123 derivative work either as part of your product's requirements
124 for function or as a bundled extra, such as an operating
125 system distribution, you may charge a fee for your product as
126 long as you also make this package or said derivative work
127 available for free separately (such as by download or link
128 back to this package's site), as you are considered to be
129 requesting a fee for your own product and the package is
130 merely included as a convenience to your users.
131 + If you offer installation of this package or a derivative work
132 as a service, you may charge a fee for the act of installation
133 as long as you also make this package or said derivative work
134 available for free (such as by download or link back to this
135 package's site), as you are considered to be requesting a fee
136 for the act of installation and not for the software you are
137 installing.
138 + The Author may also grant, in writing, other specified
139 exemptions for your particular commercial purpose that do not
140 contravene the spirit of this license or any license terms
141 this package additionally carries.
142 * In your derivative works based on this package, you may choose to
143 offer warranty support or guarantees of performance. This does not
144 in any way make the original Author legally, financially or in any
145 other respect liable for claims issued under your warranty or
146 guarantee, and you are solely responsible for the fulfillment of
147 your terms even if the Author of the work you have based your work
148 upon offers his or her own.
149 * In your derivative works based on this package, you may further
150 restrict the acceptable uses of your package or situations in which
151 it may be employed as long as you clearly state that your terms
152 apply only to your derivative work and not to the original
153 reference distribution. However, you may not countermand or ignore,
154 directly or otherwise, any restriction already made in the
155 reference distribution's license, including in this document
156 itself, in similar fashion to other licenses allowing compatible
157 licenses to co-govern a particular package's use.
158
159 What you must not do under this license
160
161 Remember that these limits apply only to redistribution of a reference
162 distribution, or to a true derivative work. If your project does not
163 include this package or code based upon it, even if it requires this
164 package to function, it is not considered subject to this license or
165 these restrictions.
166 * You must not charge a fee for purchase or rental of this package or
167 any derivative work based on this package. It is still possible to
168 use this package in a commercial environment, however -- see What
169 you are permitted to do under this license.
170 * You must not countermand or ignore, directly or otherwise, the
171 restrictions already extant in this package's license in your
172 derivative work based on it. As a corollary, you must not place
173 your derivative work under a secondary license or description of
174 terms that conflicts with it (for example, this license is not
175 compatible with the GNU Public License).
176 * You must not label any modified distribution of this package as a
177 reference or otherwise official distribution without the permission
178 of the original Author or Authors. You must clearly specify that
179 your modified work is a derivative work, including binary-only
180 releases if the original included source code and you do not even
181 if you did not modify the source prior to compilation.
182
183 What you must do under this license
184
185 * You must agree to all terms specified (agreement to which is
186 unconditionally signified by your usage, modification or
187 repurposing of this package), or to remove the package from your
188 computer and not use it further.
189 * In the absence of any specific offer for redress or assistance
190 under warranty or guarantee of performance that the Author of this
191 package might make, you must agree to accept any and all liability
192 that may come from the use of this package, proper or improper,
193 real or imagined, and certify without condition that you use this
194 product at your own risk with no guarantee of function,
195 merchantability or fitness for a particular purpose. If such offer
196 of redress or assistance is extended, it is fulfillable only by the
197 Author who extended the offer, which might not necessarily be this
198 Author, nor might it be the Authors of any packages it might be
199 based upon.
200 * If you choose to publicly redistribute this package or create a
201 derivative work based on this package, you must make it available
202 without any purchase or rental fee of any kind.
203 * If you choose to create a derivative work based on this package,
204 your derivative work must be copyrighted, and must be governed
205 under (at a minimum) the original package's license, which will
206 necessarily include all terms noted here. As such, if you choose to
207 distribute your derivative work, you must include a human-readable
208 license in your distribution containing all restrictions of use,
209 necessarily including this license, and any additional restrictions
210 the Author has mandated that do not contravene this license which
211 you and users of your derivative work must also honour.
212 * If you choose to create and distribute a derivative work based on
213 this package, your derivative work must clearly make reference to
214 this package, any other packages your work or the original work
215 might be based on, and all applicable copyrights, either in your
216 documentation, your work's standard human-readable output, or both.
217 A suggested method might be
218
219 Contains or is based on the Foo software package.
220 Copyright (C) 2112 D. Original Author. All rights reserved.
221 http://their.web.site.invalid/
222
223 Additional notes
224
225 Enforcement is the responsibility of the Author. However, violation of
226 this license may subject you to criminal and civil penalties depending
227 on your country.
228
229 This package is bound by the version of license that accompanies it.
230 Future official versions of a particular package may use a more updated
231 license, and you should always review the license before use. This
232 license's most current version is always available from the following
233 locations:
234
235 [1]http://www.floodgap.com/software/ffsl/
236 [2]gopher://gopher.floodgap.com/1/ffsl/
237
238 This license is version 1, dated 19 November 2006.
239
240 This license is copyright © 2006 Cameron Kaiser. All rights reserved.
241 The text of this license is available for re-use and re-distribution
242 under the Creative Commons. The use of the term "Floodgap Free Software
243 License" does not imply endorsement of packages using this license by
244 Floodgap Systems or by Cameron Kaiser. Modified licenses using portions
245 of these terms may refer to themselves as modified FFSL, with the
246 proviso that their modifications be clearly marked, as specified below:
247
248 [3]Creative Commons License
249 This work is licensed under a [4]Creative Commons
250 Attribution-ShareAlike 2.5 License.
251
252 Only the text of this license, and not programs covered by this
253 license, is so offered under Creative Commons.
254
255 References
256
257 1. http://www.floodgap.com/software/ffsl/
258 2. gopher://gopher.floodgap.com/1/ffsl/
259 3. http://creativecommons.org/licenses/by-sa/2.5/
260 4. http://creativecommons.org/licenses/by-sa/2.5/
0 # oysttyer
1
2 The official fork and replacement for what was once [Floodgap's TTYtter](http://www.floodgap.com/software/ttytter/).
3
4 In order to get Cameron Kaiser's blessing, we've had to change the name, take out a new API key and keep the Floodgap Free Software License.
5
6 **The master branch will be pretty much what I'm running, but that doesn't mean I've not managed to break it in someway**; Tagged releases (i.e. X.X.X) are intended to be stable. The mirror branch reflects all the historical official TTYtter updates.
7
8 See the [oysttyer User Guide](http://oysttyer.github.io/docs/userGuide.html) for usage information.
9
10 ## Switching from TTYtter
11
12 1. You have to re-authorise (you can't use your `.ttytterkey`) as we have a new API key
13 2. Move/rename your `.ttytterc` file to `.oysttyerrc`
14 3. If you use the `ttytteristas` pref it is now called `oysttyeristas`
15 4. Read the Changelog to see what's new since TTYtter 2.1
16
17 I think that's it?
18
19 ### Launching Oysttyer
20
21 Depending on how you obtain oysttyer the file could already be executable so you can launch it directly (`./oysttyer.pl`) as long as the shebang matches your path to Perl or you alter the shebang so it does. However, it's probably a lot easier just to do:
22
23 perl oysttyer.pl
24
25 ### Using your own oauthkey and oauthsecret
26
27 Since the transition from TTYtter, Twitter seem to be in the habit of muzzling us (their word for blocking write access). This is done at the oysttyer oauthkey/secret level so affects all users. As a (hopefully) temporary work-around until we can resolve this issue permanently with Twitter you can register our own app (You can call it whatever, but if you are stuck for a name call it "oysttyer-<your twitter handle>") and specify the `oauthkey` and `oauthsecret` in the `.oysttyerrc` file:
28
29 oauthkey=xxXxxXxxXXXXXxXxxxXXXxxXX
30 oauthsecret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
31
32 Be sure to avoid trailing whitespace in your key/secret. You will, of course, have to re-authorise and get a new token. I suggest taking advantage of the existing keyfile functionality in oysttyer to do this. **Important**: If you are using you own oauthkey and oauthsecret to get a new token then that token will only work with your own oauthkey and oauthsecret. Tokens are not interchangeable between oauthkeys and oauthsecrets.
33
34 ### New functionality since TTYtter 2.1
35
36 Until we catch up with the documentation, check out the changelog and commitlog, etc.
37
38 ### Notes to extension developers
39
40 1. The `TTYtter_VERSION`, `TTYtter_PATCH_VERSION` and `TTYtter_RC_NUMBER` variables are now `oysttyer_VERSION`, `oysttyer_PATCH_VERSION` and `oysttyer_RC_NUMBER`.
41 2. User-agent string has changed to `oysttyer/$oysttyer_VERSION`.
42
43 ## Recommendations
44
45 I strongly suggest, although it is by no means compulsory, tracking @oysttyer and #oysttyer as that way you become connected to a global support network.
46
47 Also, check out some available extensions:
48
49 * [oysttyer-profile](https://github.com/oysttyer/oysttyer-profile) update your profile information from within oysttyer
50 * [oysttyer-deshortify](https://github.com/oysttyer/oysttyer-deshortify) gets rid of shortlinks and displays final URLs
51 * [oysttyer-multigeo](https://github.com/oysttyer/oysttyer-multigeo) for all your geographical location needs
52
0 #!/usr/bin/perl -s
1 # TODO: Eventually we should use Getopt::Long and go back to #!/usr/bin/env perl
2 #########################################################################
3 #
4 # oysttyer v2.9 (c)2016- oysttyer organisation
5 # (c)2007-2012 cameron kaiser (and contributors).
6 # all rights reserved.
7 #
8 # https://oysttyer.github.io/
9 #
10 # distributed under the floodgap free software license
11 # http://www.floodgap.com/software/ffsl/
12 #
13 # After all, we're flesh and blood. -- Oingo Boingo
14 # If someone writes an app and no one uses it, does his code run? -- me
15 #
16 #########################################################################
17
18 require 5.005;
19
20 BEGIN {
21 # ONLY STUFF THAT MUST RUN BEFORE INITIALIZATION GOES HERE!
22 # THIS FUNCTION HAS GOTTEN TOO DAMN CLUTTERED!
23
24 # @INC = (); # wreck intentionally for testing
25 # dynamically changing PERL_SIGNALS doesn't work in Perl 5.14+ (bug
26 # 92246). we deal with this by forcing -signals_use_posix if the
27 # environment variable wasn't already set.
28 if ($] >= 5.014000 && $ENV{'PERL_SIGNALS'} ne 'unsafe') {
29 $signals_use_posix = 1;
30 } else {
31 $ENV{'PERL_SIGNALS'} = 'unsafe';
32 }
33
34 $command_line = $0; $0 = "oysttyer";
35 $oysttyer_VERSION = "2.9";
36 $oysttyer_PATCH_VERSION = 1;
37 $oysttyer_RC_NUMBER = 0; # non-zero for release candidate
38 # this is kludgy, yes.
39 $LANG = $ENV{'LANG'} || $ENV{'GDM_LANG'} || $ENV{'LC_CTYPE'} ||
40 $ENV{'ALL'};
41 $my_version_string = "${oysttyer_VERSION}.${oysttyer_PATCH_VERSION}";
42 (warn ("$my_version_string\n"), exit) if ($version);
43
44 $packet_length = 2048;
45 $space_pad = " " x $packet_length;
46 $background_is_ready = 0;
47
48 # for multi-module extension handling
49 $multi_module_mode = 0;
50 $multi_module_context = 0;
51 $muffle_server_messages = 0;
52 undef $master_store;
53 undef %push_stack;
54
55 $padded_patch_version = substr($oysttyer_PATCH_VERSION . " ", 0, 2);
56
57 %opts_boolean = map { $_ => 1 } qw(
58 ansi noansi verbose superverbose oysttyeristas noprompt
59 seven silent hold daemon script anonymous readline ssl
60 newline vcheck verify noratelimit notrack nonewrts notimeline
61 synch exception_is_maskable mentions simplestart
62 location readlinerepaint nocounter notifyquiet
63 signals_use_posix dostream nostreamreplies streamallreplies
64 nofilter showusername largeimages origimages doublespace extended
65 ); %opts_sync = map { $_ => 1 } qw(
66 ansi pause dmpause oysttyeristas verbose superverbose
67 url rlurl dmurl newline wrap notimeline lists dmidurl
68 queryurl track colourprompt colourme notrack
69 colourdm colourreply colourwarn coloursearch colourlist idurl
70 notifies filter colourdefault backload searchhits dmsenturl
71 nostreamreplies mentions wtrendurl atrendurl filterusers
72 filterats filterrts filteratonly filterflags nofilter
73 ); %opts_urls = map {$_ => 1} qw(
74 url dmurl uurl rurl wurl frurl rlurl update shorturl
75 apibase queryurl idurl delurl dmdelurl favsurl
76 favurl favdelurl followurl leaveurl
77 muteurl unmuteurl
78 dmupdate credurl blockurl blockdelurl friendsurl
79 modifyliurl adduliurl delliurl getliurl getlisurl getfliurl
80 creliurl delliurl deluliurl crefliurl delfliurl
81 getuliurl getufliurl dmsenturl rturl rtsbyurl dmidurl
82 statusliurl followliurl leaveliurl followersurl
83 oauthurl oauthauthurl oauthaccurl oauthbase wtrendurl
84 atrendurl frupdurl lookupidurl rtsofmeurl
85 ); %opts_secret = map { $_ => 1} qw(
86 superverbose oysttyeristas
87 ); %opts_comma_delimit = map { $_ => 1 } qw(
88 lists notifytype notifies filterflags filterrts filterats
89 filterusers filteratonly
90 ); %opts_space_delimit = map { $_ => 1 } qw(
91 track
92 );
93
94 %opts_can_set = map { $_ => 1 } qw(
95 url pause dmurl dmpause superverbose ansi verbose
96 update uurl rurl wurl avatar oysttyeristas frurl track
97 rlurl noprompt shorturl newline wrap verify autosplit
98 notimeline queryurl colourprompt colourme
99 colourdm colourreply colourwarn coloursearch colourlist idurl
100 urlopen delurl notrack dmdelurl favsurl
101 favurl favdelurl slowpost notifies filter colourdefault
102 followurl leaveurl dmupdate mentions backload
103 lat long location searchhits blockurl blockdelurl woeid
104 nocounter linelength quotelinelength friendsurl followersurl lists
105 modifyliurl adduliurl delliurl getliurl getlisurl getfliurl
106 creliurl delliurl deluliurl crefliurl delfliurl atrendurl
107 getuliurl getufliurl dmsenturl rturl rtsbyurl wtrendurl
108 statusliurl followliurl leaveliurl dmidurl nostreamreplies
109 frupdurl filterusers filterats filterrts filterflags
110 filteratonly nofilter rtsofmeurl largeimages origimages extended
111 video_bitrate
112 ); %opts_others = map { $_ => 1 } qw(
113 lynx curl seven silent maxhist noansi hold status
114 daemon timestamp twarg user anonymous script readline
115 leader ssl rc norc vcheck apibase notifytype exts
116 nonewrts synch runcommand authtype oauthkey oauthsecret
117 tokenkey tokensecret credurl keyf lockf readlinerepaint
118 simplestart exception_is_maskable oldperl notco
119 notify_tool_path oauthurl oauthauthurl oauthaccurl oauthbase
120 signals_use_posix dostream eventbuf replacement_newline
121 replacement_carriagereturn streamallreplies showusername
122 doublespace
123 ); %valid = (%opts_can_set, %opts_others);
124 $rc = (defined($rc) && length($rc)) ? $rc : "";
125 unless ($norc) {
126 my $rcf =
127 ($rc =~ m#^/#) ? $rc : "$ENV{'HOME'}/.oysttyerrc${rc}";
128 if (open(W, $rcf)) {
129 while(<W>) {
130 chomp;
131 next if (/^\s*$/ || /^#/);
132 s/^-//;
133 ($key, $value) = split(/\=/, $_, 2);
134 if ($key eq 'rc') {
135 warn "** that's stupid, setting rc in an rc file\n";
136 } elsif ($key eq 'norc') {
137 warn "** that's dumb, using norc in an rc file\n";
138 } elsif (length $$key) {
139 ; # carry on
140 } elsif ($valid{$key} && !length($$key)) {
141 $$key = $value;
142 } elsif ($key =~ /^extpref_/) {
143 $$key = $value;
144 } elsif (!$valid{$key}) {
145 warn "** setting $key not supported in this version\n";
146 }
147 }
148 close(W);
149 } elsif (length($rc)) {
150 die("couldn't access rc file $rcf: $!\n".
151 "to use defaults, use -norc or don't specify the -rc option.\n\n");
152 }
153 }
154 warn "** -twarg is deprecated\n" if (length($twarg));
155 $seven ||= 0;
156 $oldperl ||= 0;
157 $parent = $$;
158 $script = 1 if (length($runcommand));
159 $supreturnto = $verbose + 0;
160 $postbreak_time = 0;
161 $postbreak_count = 0;
162 # Want to keep original behaviour as well though
163 $newline ||= 0;
164 $replacement_newline ||= $seven ? ' [NL] ' : " \x{2424} ";
165 $replacement_carriagereturn ||= $seven ? ' [CR] ' : " \x{240D} ";
166
167 # our minimum official support is now 5.8.6.
168 if ($] < 5.008006 && !$oldperl) {
169 die(<<"EOF");
170
171 *** you are using a version of Perl in "extended" support: $] ***
172 the minimum tested version of Perl now required by oysttyer is 5.8.6.
173
174 Perl 5.005 thru 5.8.5 probably can still run oysttyer, but they are not
175 tested with it. if you want to suppress this warning, specify -oldperl on
176 the command line, or put oldperl=1 in your .oysttyerrc. bug patches will
177 still be accepted for older Perls; see the oysttyer home page for info.
178
179 for Perl 5.005, remember to also specify -seven.
180
181 EOF
182 }
183
184 # defaults that our extensions can override
185 $last_id = 0;
186 $last_dm = 0;
187 # a correct fix for -daemon would make this unlimited, but this
188 # is good enough for now.
189 $print_max ||= ($daemon) ? 999999 : 250; # shiver
190
191 $suspend_output = -1;
192
193 # try to find an OAuth keyfile if we haven't specified key+secret
194 # no worries if this fails; we could be Basic Auth, after all
195 $whine = (length($keyf)) ? 1 : 0;
196 $keyf ||= "$ENV{'HOME'}/.oysttyerkey";
197 $keyf = "$ENV{'HOME'}/.oysttyerkey${keyf}" if ($keyf !~ m#/#);
198 $attempted_keyf = $keyf;
199 if (!$oauthwizard && (
200 #!length($oauthkey) ||
201 #!length($oauthsecret) ||
202 !length($tokenkey) ||
203 !length($tokensecret) )
204 ) {
205 my $keybuf = '';
206 if(open(W, $keyf)) {
207 while(<W>) {
208 chomp;
209 s/\s+//g;
210 $keybuf .= $_;
211 }
212 close(W);
213 my (@pairs) = split(/\&/, $keybuf);
214 foreach(@pairs) {
215 my (@pair) = split(/\=/, $_, 2);
216 $oauthkey = $pair[1]
217 if ($pair[0] eq 'ck') && !length($oauthkey);# && $pair[1] ne 'X');
218 $oauthsecret = $pair[1]
219 if ($pair[0] eq 'cs') && !length($oauthsecret);# && $pair[1] ne 'X');
220 $tokenkey = $pair[1]
221 if ($pair[0] eq 'at');
222 $tokensecret = $pair[1]
223 if ($pair[0] eq 'ats');
224 }
225 die("** tried to load OAuth tokens from $keyf\n".
226 " but it seems corrupt or incomplete. please see the documentation,\n".
227 " or delete the file so that we can try making your keyfile again.\n")
228 if ((!length($oauthkey) ||
229 !length($oauthsecret) ||
230 !length($tokenkey) ||
231 !length($tokensecret)));
232 } else {
233 die("** couldn't open keyfile $keyf: $!\n".
234 "if you want to run the OAuth wizard to create this file, add ".
235 "-oauthwizard\n")
236 if ($whine);
237 $keyf = ''; # i.e., we loaded nothing from a key file
238 }
239 }
240
241 # try to init Term::ReadLine if it was requested
242 # (shakes fist at @br3nda, it's all her fault)
243 %readline_completion = ();
244 if ($readline && !$silent && !$script) {
245 $ENV{"PERL_RL"} = "TTYtter" if (!length($ENV{'PERL_RL'}));
246 eval
247 'use Term::ReadLine; $termrl = new Term::ReadLine ("TTYtter", \*STDIN, \*STDOUT)'
248 || die(
249 "$@\nthis perl doesn't have ReadLine. don't use -readline.\n");
250 $stdout = $termrl->OUT || \*STDOUT;
251 $stdin = $termrl->IN || \*STDIN;
252 $readline = '' if ($readline eq '1');
253 $readline =~ s/^"//; # for optimizer
254 $readline =~ s/"$//;
255 #$termrl->Attribs()->{'autohistory'} = undef; # not yet
256 (%readline_completion) = map {$_ => 1} split(/\s+/, $readline);
257 %original_readline = %readline_completion;
258 # readline repaint can't be tested here. we cache our
259 # result later.
260 } else {
261 $stdout = \*STDOUT;
262 $stdin = \*STDIN;
263 }
264 $wrapseq = 0;
265 $lastlinelength = -1;
266
267 print $stdout "$leader\n" if (length($leader));
268
269 # state information
270 $lasttwit = '';
271 $lastpostid = 0;
272
273 # stub namespace for multimodules and (eventually) state saving
274 undef %store;
275 $store = \%store;
276
277 $pack_magic = ($] < 5.006) ? '' : "U0";
278 $utf8_encode = sub { ; };
279 $utf8_decode = sub { ; };
280 unless ($seven) {
281 eval
282 'use utf8;binmode($stdin,":utf8");binmode($stdout,":utf8");return 1' ||
283 die("$@\nthis perl doesn't fully support UTF-8. use -seven.\n");
284
285 # this is for the prinput utf8 validator.
286 # adapted from http://mail.nl.linux.org/linux-utf8/2003-03/msg00087.html
287 # eventually this will be removed when 5.6.x support is removed,
288 # and Perl will do the UTF-8 validation for us.
289 $badutf8='[\x00-\x7f][\x80-\xbf]+|^[\x80-\xbf]+|'.
290 '[\xc0-\xdf][\x00-\x7f\xc0-\xff]|'.
291 '[\xc0-\xdf][\x80-\xbf]{2}|'.
292 '[\xe0-\xef][\x80-\xbf]{0,1}[\x00-\x7f\xc0-\xff]|'.
293 '[\xe0-\xef][\x80-\xbf]{3}|'.
294 '[\xf0-\xf7][\x80-\xbf]{0,2}[\x00-\x7f\xc0-\xff]|'.
295 '[\xf0-\xf7][\x80-\xbf]{4}|'.
296 '[\xf8-\xfb][\x80-\xbf]{0,3}[\x00-\x7f\xc0-\xff]|'.
297 '[\xf8-\xfb][\x80-\xbf]{5}|'.
298 '[\xfc-\xfd][\x80-\xbf]{0,4}[\x00-\x7f\xc0-\xff]|'.
299 '\xed[\xa0-\xbf][\x80-\xbf]|'.
300 '\xef\xbf[\xbe-\xbf]|'.
301 '[\xf0-\xf7][\x8f,\x9f,\xaf,\xbf]\xbf[\xbe-\xbf]|'.
302 '\xfe|\xff|'.
303 '[\xc0-\xc1][\x80-\xbf]|'.
304 '\xe0[\x80-\x9f][\x80-\xbf]|'.
305 '\xf0[\x80-\x8f][\x80-\xbf]{2}|'.
306 '\xf8[\x80-\x87][\x80-\xbf]{3}|'.
307 '\xfc[\x80-\x83][\x80-\xbf]{4}'; # gah!
308
309 eval <<'EOF';
310 $utf8_encode = sub { utf8::encode(shift); };
311 $utf8_decode = sub { utf8::decode(shift); };
312 EOF
313 }
314 $wraptime = sub { my $x = shift; return ($x, $x); };
315 if ($timestamp) {
316 my $fail = "-- can't use custom timestamps.\nspecify -timestamp by itself to use Twitter's without module.\n";
317 if (length($timestamp) > 1) { # pattern specified
318 eval 'use Date::Parse;return 1' ||
319 die("$@\nno Date::Parse $fail");
320 eval 'use Date::Format;return 1' ||
321 die("$@\nno Date::Format $fail");
322 $timestamp = "%Y-%m-%d %k:%M:%S"
323 if ($timestamp eq "default" ||
324 $timestamp eq "def");
325 $wraptime = sub {
326 my $time = str2time(shift);
327 my $stime = time2str($timestamp, $time);
328 return ($time, $stime);
329 };
330 }
331 }
332 }
333 END {
334 &killkid unless ($in_backticks || $in_buffer); # this is disgusting
335 }
336
337 #### COMMON STARTUP ####
338
339 # if we requested POSIX signals, or we NEED posix signals (5.14+), we
340 # must check if we have POSIX signals actually
341 if ($signals_use_posix) {
342 eval 'use POSIX';
343 # God help the system that doesn't have SIGTERM
344 $j = eval 'return POSIX::SIGTERM' ;
345 die(<<"EOF") if (!(0+$j));
346 *** death permeates me ***
347 your configuration requires using POSIX signalling (either Perl 5.14+ or
348 you specifically asked with -signals_use_posix). however, either you don't
349 have POSIX.pm, or it doesn't work.
350
351 oysttyer requires 'unsafe' Perl signals (which are of course for its
352 purposes perfectly safe). unfortunately, due to Perl bug 92246 5.14+ must
353 use POSIX.pm, or have the switch set before starting oysttyer. run one of
354
355 export PERL_SIGNALS=unsafe # sh, bash, ksh, etc.
356 setenv PERL_SIGNALS unsafe # csh, tcsh, etc.
357
358 and restart oysttyer, or use Perl 5.12 or earlier (without specifying
359 -signals_use_posix).
360 EOF
361 }
362
363 # do we have POSIX::Termios? (usually we do)
364 eval 'use POSIX; $termios = new POSIX::Termios;';
365 print $stdout "-- termios test: $termios\n" if ($verbose);
366
367 # check the TRLT version. versions < 1.3 won't work with 2.0.
368 if ($termrl && $termrl->ReadLine eq 'Term::ReadLine::TTYtter') {
369 eval '$trlv = $termrl->Version;';
370 die (<<"EOF") if (length($trlv) && 0+$trlv < 1.3);
371 *** death permeates me ***
372 you need to upgrade your Term::ReadLine::TTYtter to at least version 1.3
373 to use oysttyer 2.x, or bad things will happen such as signal mismatches,
374 unexpected quits, and dogs and cats living peacefully in the same house.
375
376 EOF
377 print $stdout "** t.co support needs Term::ReadLine:TTYtter 1.4+ (-notco to ignore)\n"
378 if (length($trlv) && !$notco && 0+$trlv < 1.4);
379 }
380
381 # try to get signal numbers for SIG* from POSIX. use internals if failed.
382 eval 'use POSIX; $SIGUSR1 = POSIX::SIGUSR1; $SIGUSR2 = POSIX::SIGUSR2; $SIGHUP = POSIX::SIGHUP; $SIGTERM = POSIX::SIGTERM';
383 # from <sys/signal.h>
384 $SIGHUP ||= 1;
385 $SIGTERM ||= 15;
386 $SIGUSR1 ||= 30;
387 $SIGUSR2 ||= 31;
388
389 # wrap warning
390 die(
391 "** dude, what the hell kind of terminal can't handle a 5 character line?\n")
392 if ($wrap > 1 && $wrap < 5);
393 print $stdout "** warning: prompts not wrapped for wrap < 70\n"
394 if ($wrap > 1 && $wrap < 70);
395
396 # reject stupid combinations
397 die("-largeimages and -origimages cannot be used together.\n")
398 if ($largeimages && $origimages);
399 die("you can't use automatic ratelimits with -noratelimit.\nuse -pause=#sec\n")
400 if ($noratelimit && $pause eq 'auto');
401 die("you can't use -synch with -script or -daemon.\n")
402 if ($synch && ($script || $daemon));
403 die("-script and -daemon cannot be used together.\n")
404 if ($script && $daemon);
405
406 # set up menu codes and caches
407 $is_background = 0;
408 $alphabet = "abcdefghijkLmnopqrstuvwxyz";
409 %store_hash = ();
410 $mini_split = 250; # i.e., 10 tweets for the mini-menu (/th)
411 # leaving 50 tweets for the foreground temporary menus
412 $tweet_counter = 0;
413 %dm_store_hash = ();
414 $dm_counter = 0;
415 %id_cache = ();
416 %filter_next = ();
417
418 # set up threading management
419 $in_reply_to = 0;
420 $expected_tweet_ref = undef;
421
422 # interpret -script at this level
423 if ($script) {
424 $noansi = $noprompt = 1;
425 $silent = ($verbose) ? 0 : 1;
426 $pause = $vcheck = $slowpost = $verify = 0;
427 }
428
429 ### now instantiate the oysttyer dynamic API ###
430 ### based off the defaults later in script. ####
431
432 # first we need to load any extensions specified by -exts.
433 if (length($exts) && $exts ne '0') {
434 $multi_module_mode = -1; # mark as loader stage
435
436 print "** attempting to load extensions\n" unless ($silent);
437 # unescape \,
438 $j=0; $xstring = "ESCAPED_STRING";
439 while($exts =~ /$xstring$j/) { $j++; }
440 $xstring .= $j;
441 $exts =~ s/\\,/$xstring/g;
442 foreach $file (split(/,/, $exts)) {
443 #TODO
444 # wildcards?
445 $file =~ s/$xstring/,/g;
446 print "** loading $file\n" unless ($silent);
447
448 die("** sorry, you cannot load the same extension twice.\n")
449 if ($master_store->{$file}->{'loaded'});
450
451 # prepare its working space in $store and load the module
452 $master_store->{$file} = { 'loaded' => 1 };
453 $store = \%{ $master_store->{$file} };
454 $EM_DONT_CARE = 0;
455 $EM_SCRIPT_ON = 1;
456 $EM_SCRIPT_OFF = -1;
457 $extension_mode = $EM_DONT_CARE;
458 die("** $file not found: $!\n") if (! -r "$file");
459 require $file; # and die if bad
460 die("** $file failed to load: $@\n") if ($@);
461 die("** consistency failure: reference failure on $file\n")
462 if (!$store->{'loaded'});
463
464 # check type of extension (interactive or non-interactive). if
465 # we are in the wrong mode, bail out.
466 if ($extension_mode) {
467 die(
468 "** this extension requires -script. this may conflict with other extensions\n".
469 " you are loading, which may have their own requirements.\n")
470 if ($extension_mode == $EM_SCRIPT_ON && !$script);
471 die(
472 "** this extension cannot work with -script. this may conflict with other\n".
473 " extensions you are loading, which may have their own requirements.\n")
474 if ($extension_mode == $EM_SCRIPT_OFF && $script);
475 }
476
477 # pick off all the subroutine references it makes for storage
478 # in an array to iterate and chain over later.
479
480 # these methods are multi-module safe
481 foreach $arry (qw(
482 handle exception tweettype conclude dmhandle dmconclude
483 heartbeat precommand prepost postpost addaction
484 eventhandle listhandle userhandle shutdown)) {
485 if (defined($$arry)) {
486 $aarry = "m_$arry";
487 push(@$aarry, [ $file, $$arry ]);
488 undef $$arry;
489 }
490 }
491 # these methods are NOT multi-module safe
492 # if a extension already hooked one of
493 # these and another extension tries to hook it, fatal error.
494 foreach $arry (qw(
495 getpassword prompt main autocompletion)) {
496 if (defined($$arry)) {
497 $sarry = "l_$arry";
498 if (defined($$sarry)) {
499 die(
500 "** double hook of unsafe method \"$arry\" -- you cannot use this extension\n".
501 " with the other extensions you are loading. see the documentation.\n");
502 }
503 $$sarry = $$arry;
504 undef $$arry;
505 }
506 }
507 }
508 # success! enable multi-module support in the oysttyer API and then
509 # dispatch calls through the multi-module system instead.
510 $multi_module_mode = 1; # mark as completed loader
511
512 $handle = \&multihandle;
513 $exception = \&multiexception;
514 $tweettype = \&multitweettype;
515 $conclude = \&multiconclude;
516 $dmhandle = \&multidmhandle;
517 $dmconclude = \&multidmconclude;
518 $heartbeat = \&multiheartbeat;
519 $precommand = \&multiprecommand;
520 $prepost = \&multiprepost;
521 $postpost = \&multipostpost;
522 $addaction = \&multiaddaction;
523 $shutdown = \&multishutdown;
524 $userhandle = \&multiuserhandle;
525 $listhandle = \&multilisthandle;
526 $eventhandle = \&multieventhandle;
527
528 } else {
529 # the old API single-end-point system
530
531 $multi_module_mode = 0; # not executing multi module endpoints
532
533 $handle = \&defaulthandle;
534 $exception = \&defaultexception;
535 $tweettype = \&defaulttweettype;
536 $conclude = \&defaultconclude;
537 $dmhandle = \&defaultdmhandle;
538 $dmconclude = \&defaultdmconclude;
539 $heartbeat = \&defaultheartbeat;
540 $precommand = \&defaultprecommand;
541 $prepost = \&defaultprepost;
542 $postpost = \&defaultpostpost;
543 $addaction = \&defaultaddaction;
544 $shutdown = \&defaultshutdown;
545 $userhandle = \&defaultuserhandle;
546 $listhandle = \&defaultlisthandle;
547 $eventhandle = \&defaulteventhandle;
548 }
549
550 # unsafe methods use the single-end-point
551 $prompt = $l_prompt || \&defaultprompt;
552 $main = $l_main || \&defaultmain;
553 $getpassword = $l_getpassword || \&defaultgetpassword;
554
555 # $autocompletion is special:
556 if ($termrl) {
557 $termrl->Attribs()->{'completion_function'} =
558 $l_autocompletion || \&defaultautocompletion;
559 }
560
561 # fetch_id is based off last_id, if an extension set it
562 $fetch_id = $last_id || 0;
563
564 # validate the notify method the user chose, if any.
565 # we can't do this in BEGIN, because it may not be instantiated yet,
566 # and we have to do it after loading modules because it might be in one.
567 @notifytypes = ();
568 if (length($notifytype) && $notifytype ne '0' &&
569 $notifytype ne '1' && !$status) {
570 # NOT $script! scripts have a use case for notifiers!
571
572 %dupenet = ();
573 foreach $nt (split(/\s*,\s*/, $notifytype)) {
574 $fnt="notifier_${nt}";
575 (warn("** duplicate notification $nt was ignored\n"), next)
576 if ($dupenet{$fnt});
577 eval 'return &$fnt(undef)' ||
578 die("** invalid notification framework $nt: $@\n");
579 $dupenet{$fnt}=1;
580 }
581 @notifytypes = keys %dupenet;
582 $notifytype = join(',', @notifytypes);
583 # warning if someone didn't tell us what notifies they wanted.
584 warn "-- warning: you specified -notifytype, but no -notifies\n"
585 if (!$silent && !length($notifies));
586 }
587
588 # set up track tags
589 if (length($tquery) && $tquery ne '0') {
590 my $xtquery = &tracktags_tqueryurlify($tquery);
591 die("** custom tquery is over 140 length: $xtquery\n")
592 if (length($xtquery) > 139);
593 @trackstrings = ($xtquery);
594 } else {
595 &tracktags_makearray;
596 }
597
598 # compile filterflags
599 &filterflags_compile;
600
601 # compile filters
602 exit(1) if (!&filter_compile);
603 $filterusers_sub = &filteruserlist_compile(undef, $filterusers);
604 $filterrts_sub = &filteruserlist_compile(undef, $filterrts);
605 $filteratonly_sub = &filteruserlist_compile(undef, $filteratonly);
606 exit(1) if (!&filterats_compile);
607
608 # compile lists
609 exit(1) if (!&list_compile);
610
611 # finally, compile notifies. we do this regardless of notifytype, so that
612 # an extension can look at it if it wants to.
613 &notify_compile;
614
615 # check that we are using a sensible authtype, based on our guessed user agent
616 $authtype ||= "oauth";
617 die("** supported authtypes are basic or oauth only.\n")
618 if ($authtype ne 'basic' && $authtype ne 'oauth');
619
620 if ($termrl) {
621 $streamout = $stdout; # this is just simpler instead of dupping
622 warn(<<"EOF") if ($] < 5.006);
623 ***********************************************************
624 ** -readline may not function correctly on Perls < 5.6.0 **
625 ***********************************************************
626 EOF
627 print $stdout "-- readline using ".$termrl->ReadLine."\n";
628 } else {
629 # dup $stdout for benefit of various other scripts
630 open(DUPSTDOUT, ">&STDOUT") ||
631 warn("** warning: could not dup $stdout: $!\n");
632 binmode(DUPSTDOUT, ":utf8") unless ($seven);
633 $streamout = \*DUPSTDOUT;
634 }
635 if ($silent) {
636 close($stdout);
637 open($stdout, ">>/dev/null"); # KLUUUUUUUDGE
638 }
639
640 # after this point, die() may cause problems
641
642 # initialize our route back out so background can talk to foreground
643 pipe(W, P) || die("pipe() error [or your Perl doesn't support it]: $!\n");
644 select(P); $|++;
645
646 # default command line options
647
648 $anonymous ||= 0;
649 $ssl ||= 1;
650 die("** -anonymous is no longer supported with Twitter (you must use -apibase also)\n")
651 if ($anonymous && !length($apibase));
652 undef $user if ($anonymous);
653 print $stdout "-- using SSL for default URLs.\n" if ($ssl);
654 $http_proto = ($ssl) ? 'https' : 'http';
655
656 $lat ||= undef;
657 $long ||= undef;
658 $location ||= 0;
659 $linelength ||= 140;
660 $quotelinelength ||= 116;
661 $tco_length ||= 23; # The number of characters that t.co links require
662 $dm_text_character_limit ||= 10000;
663 $oauthbase ||= $apibase || "${http_proto}://api.twitter.com";
664 # this needs to be AFTER oauthbase so that apibase can set oauthbase.
665 $apibase ||= "${http_proto}://api.twitter.com/1.1";
666 $nonewrts ||= 0;
667
668 # special case: if we explicitly refuse backload, don't load initially.
669 $backload = 30 if (!defined($backload)); # zero is valid!
670 $dont_refresh_first_time = 1 if (!$backload);
671
672 $searchhits ||= 20;
673 $url ||= "${apibase}/statuses/home_timeline.json";
674
675 $oauthurl ||= "${oauthbase}/oauth/request_token";
676 $oauthauthurl ||= "${oauthbase}/oauth/authorize";
677 $oauthaccurl ||= "${oauthbase}/oauth/access_token";
678
679 $credurl ||= "${apibase}/account/verify_credentials.json";
680 $update ||= "${apibase}/statuses/update.json";
681 $rurl ||= "${apibase}/statuses/mentions_timeline.json";
682 $uurl ||= "${apibase}/statuses/user_timeline.json";
683 $idurl ||= "${apibase}/statuses/show.json";
684 $delurl ||= "${apibase}/statuses/destroy/%I.json";
685
686 $rturl ||= "${apibase}/statuses/retweet";
687 $rtsbyurl ||= "${apibase}/statuses/retweets/%I.json";
688 $rtsofmeurl ||= "${apibase}/statuses/retweets_of_me.json";
689
690 $wurl ||= "${apibase}/users/show.json";
691
692 $frurl ||= "${apibase}/friendships/show.json";
693 $followurl ||= "${apibase}/friendships/create.json";
694 $leaveurl ||= "${apibase}/friendships/destroy.json";
695 $blockurl ||= "${apibase}/blocks/create.json";
696 $blockdelurl ||= "${apibase}/blocks/destroy.json";
697 $friendsurl ||= "${apibase}/friends/ids.json";
698 $followersurl ||= "${apibase}/followers/ids.json";
699 $frupdurl ||= "${apibase}/friendships/update.json";
700 $lookupidurl ||= "${apibase}/users/lookup.json";
701
702 $muteurl ||= "${apibase}/mutes/users/create.json";
703 $unmuteurl ||= "${apibase}/mutes/users/destroy.json";
704
705 $rlurl ||= "${apibase}/application/rate_limit_status.json";
706
707 $dmurl ||= "${apibase}/direct_messages.json";
708 $dmsenturl ||= "${apibase}/direct_messages/sent.json";
709 $dmupdate ||= "${apibase}/direct_messages/new.json";
710 $dmdelurl ||= "${apibase}/direct_messages/destroy.json";
711 $dmidurl ||= "${apibase}/direct_messages/show.json";
712
713 $favsurl ||= "${apibase}/favorites/list.json";
714 $favurl ||= "${apibase}/favorites/create.json";
715 $favdelurl ||= "${apibase}/favorites/destroy.json";
716
717 $getlisurl ||= "${apibase}/lists/list.json";
718 $creliurl ||= "${apibase}/lists/create.json";
719 $delliurl ||= "${apibase}/lists/destroy.json";
720 $modifyliurl ||= "${apibase}/lists/update.json";
721 $deluliurl ||= "${apibase}/lists/members/destroy_all.json";
722 $adduliurl ||= "${apibase}/lists/members/create_all.json";
723 $getuliurl ||= "${apibase}/lists/memberships.json";
724 $getufliurl ||= "${apibase}/lists/subscriptions.json";
725 $delfliurl ||= "${apibase}/lists/subscribers/destroy.json";
726 $crefliurl ||= "${apibase}/lists/subscribers/create.json";
727 $getfliurl ||= "${apibase}/lists/subscribers.json";
728 $getliurl ||= "${apibase}/lists/members.json";
729 $statusliurl ||= "${apibase}/lists/statuses.json";
730
731 $streamurl ||= "https://userstream.twitter.com/1.1/user.json";
732 $dostream ||= 0;
733 $eventbuf ||= 0;
734
735 $queryurl ||= "${apibase}/search/tweets.json";
736 # no more $trendurl in 2.1.
737 $wtrendurl ||= "${apibase}/trends/place.json";
738 $atrendurl ||= "${apibase}/trends/closest.json";
739
740 # pick ONE!
741 #$shorturl ||= "http://api.tr.im/v1/trim_simple?url=";
742 $shorturl ||= "https://is.gd/create.php?format=simple&url=";
743
744 # figure out the domain to stop shortener loops
745 &generate_shortdomain;
746
747 $pause = (($anonymous) ? 120 : "auto") if (!defined $pause);
748 # NOT ||= ... zero is a VALID value!
749 $superverbose ||= 0;
750 $avatar ||= "";
751 $urlopen ||= 'echo %U';
752 $hold ||= 0;
753 $holdhold ||= 0;
754 $daemon ||= 0;
755 $maxhist ||= 19;
756 undef $shadow_history;
757 $timestamp ||= 0;
758 $noprompt ||= 0;
759 $slowpost ||= 0;
760 $twarg ||= undef;
761
762 $verbose ||= $superverbose;
763 $dmpause = 4 if (!defined $dmpause); # NOT ||= ... zero is a VALID value!
764 $dmpause = 0 if ($anonymous);
765 $dmpause = 0 if ($pause eq '0');
766 $ansi = ($noansi) ? 0 :
767 (($ansi || $ENV{'TERM'} eq 'ansi' || $ENV{'TERM'} eq 'xterm-color')
768 ? 1 : 0);
769 $showusername ||= 0;
770 $largeimages ||= 0;
771 $origimages ||= 0;
772 $doublespace ||= 0;
773 $extended ||= 0;
774 $video_bitrate ||= 'highest';
775 if ($extended) {
776 $tweet_mode = "extended";
777 $display_mode = "full_text";
778 } else {
779 $tweet_mode = "compatibility";
780 $display_mode = "text";
781 }
782
783 # synch overrides these options.
784 if ($synch) {
785 $pause = 0;
786 $dmpause = ($dmpause) ? 1 : 0;
787 }
788
789 $dmcount = $dmpause;
790 $lastshort = undef;
791
792 # ANSI sequences
793 $colourprompt ||= "CYAN";
794 $colourme ||= "YELLOW";
795 $colourdm ||= "GREEN";
796 $colourreply ||= "RED";
797 $colourwarn ||= "MAGENTA";
798 $coloursearch ||= "CYAN";
799 $colourlist ||= "OFF";
800 $colourdefault ||= "OFF";
801 $ESC = pack("C", 27);
802 $BEL = pack("C", 7);
803 &generate_ansi;
804
805 # to force unambiguous bareword interpretation
806 $true = 'true';
807 sub true { return 'true'; }
808 $false = 'false';
809 sub false { return 'false'; }
810 $null = undef;
811 sub null { return undef; }
812
813 select($stdout); $|++;
814
815 # figure out what our user agent should be
816 if ($lynx) {
817 if (length($lynx) > 1 && -x "/$lynx") {
818 $wend = $lynx;
819 print $stdout "Lynx forced to $wend\n";
820 } else {
821 $wend = &wherecheck("trying to find Lynx", "lynx",
822 "specify -curl to use curl instead, or just let oysttyer autodetect stuff.\n");
823 }
824 } else {
825 if (length($curl) > 1 && -x "/$curl") {
826 $wend = $curl;
827 print $stdout "cURL forced to $wend\n";
828 } else {
829 $wend = (($curl) ? &wherecheck("trying to find cURL", "curl",
830 "specify -lynx to use Lynx instead, or just let oysttyer autodetect stuff.\n")
831 : &wherecheck("trying to find cURL", "curl"));
832 if (!$curl && !length($wend)) {
833 $wend = &wherecheck("failed. trying to find Lynx",
834 "lynx",
835 "you must have either Lynx or cURL installed to use oysttyer.\n")
836 if (!length($wend));
837 $lynx = 1;
838 } else {
839 $curl = 1;
840 }
841 }
842 }
843 $baseagent = $wend;
844
845 # whoops, no Lynx here if we are not using Basic Auth
846 die(
847 "sorry, OAuth is not currently supported with Lynx.\n".
848 "you must use SSL cURL, or specify -authtype=basic.\n")
849 if ($lynx && $authtype ne 'basic' && !$anonymous);
850
851 # streaming API has multiple prereqs. not fatal; we just fall back on the
852 # REST API if not there.
853 unless($status) {
854 if (!$dostream || $authtype eq 'basic' || !$ssl || $script || $anonymous || $synch) {
855 $reason = (!$dostream) ? "(no -dostream)"
856 : ($script) ? "(-script)"
857 : (!$ssl) ? "(no SSL)"
858 : ($anonymous) ? "(-anonymous)"
859 : ($synch) ? "(-synch)"
860 : ($authtype eq 'basic') ? "(no OAuth)"
861 : "(it's funkatron's fault)";
862 print $stdout
863 "-- Streaming API disabled $reason (oysttyer will use REST API only)\n";
864 $dostream = 0;
865 } else {
866 print $stdout "-- Streaming API enabled\n";
867
868 # streams change mentions behaviour; we get them automatically.
869 # warn the user if the current settings are suboptimal.
870 if ($mentions) {
871 if ($nostreamreplies) {
872 print $stdout
873 "** warning: -mentions and -nostreamreplies are very inefficient together\n";
874 } else {
875 print $stdout
876 "** warning: -mentions not generally needed in Streaming mode\n";
877 }
878 }
879 }
880 } else { $dostream = 0; } # -status suppresses streaming
881 if (!$dostream && $streamallreplies) {
882 print $stdout
883 "** warning: -streamallreplies only works in Streaming mode\n";
884 }
885
886 # create and cache the logic for our selected user agent
887 if ($lynx) {
888 $simple_agent = "$baseagent -nostatus -source";
889
890 @wend = ('-nostatus');
891 @wind = (@wend, '-source'); # GET agent
892 @wend = (@wend, '-post_data'); # POST agent
893 # we don't need to have the request signed by Lynx right now;
894 # it doesn't know how to pass custom headers. so this is simpler.
895 $stringify_args = sub {
896 my $basecom = shift;
897 my $resource = shift;
898 my $data = shift;
899 my $dont_do_auth = shift;
900 my $k = join("\n", @_);
901
902 # if resource is an arrayref, then it's a GET with URL
903 # and args (mostly generated by &grabjson)
904 $resource = join('?', @{ $resource })
905 if (ref($resource) eq 'ARRAY');
906 die("wow, we have a bug: Lynx only works with Basic Auth\n")
907 if ($authtype ne 'basic' && !$dont_do_auth);
908 $k = "-auth=".$mytoken.':'.$mytokensecret."\n".$k
909 unless ($dont_do_auth);
910 $k .= "\n";
911 $basecom = "$basecom \"$resource\" -";
912 return ($basecom, $k, $data);
913 };
914 } else {
915 $simple_agent = "$baseagent -s -m 20";
916
917 @wend = ('-s', '-m', '20', '-A', "oysttyer/$oysttyer_VERSION",
918 '--http1.1', '-H', 'Expect:');
919 @wind = @wend;
920 $stringify_args = sub {
921 my $basecom = shift;
922 my $resource = shift;
923 my $data = shift;
924 my $dont_do_auth = shift;
925 my $p;
926 my $l = '';
927
928 foreach $p (@_) {
929 if ($p =~ /^-/) {
930 $l .= "\n" if (length($l));
931 $l .= "$p ";
932 next;
933 }
934 $l .= $p;
935 }
936 $l .= "\n";
937
938 # sign our request (Basic Auth or oAuth)
939 unless ($dont_do_auth) {
940 if ($authtype eq 'basic') {
941 $l .= "-u ".$mytoken.":".$mytokensecret."\n";
942 } else {
943 my $nonce;
944 my $timestamp;
945 my $sig;
946 my $verifier = '';
947 my $header;
948 my $ttoken = (length($mytoken) ?
949 (' oauth_token=\\"'.$mytoken.'\\",') :
950 '');
951
952 ($timestamp, $nonce, $sig, $verifier) =
953 &signrequest($resource, $data);
954 $header = <<"EOF";
955 -H "Authorization: OAuth oauth_nonce=\\"$nonce\\", oauth_signature_method=\\"HMAC-SHA1\\", oauth_timestamp=\\"$timestamp\\", oauth_consumer_key=\\"$oauthkey\\", oauth_signature=\\"$sig\\",${ttoken}${verifier} oauth_version=\\"1.0\\""
956 EOF
957 print $stdout $header if ($superverbose);
958 $l .= $header;
959 }
960 }
961
962 # if resource is an arrayref, then it's a GET with URL
963 # and args (mostly generated by &grabjson)
964 $resource = join('?', @{ $resource })
965 if (ref($resource) eq 'ARRAY');
966 $l .= "url = \"$resource\"\n";
967 $l .= "data = \"$data\"\n" if length($data);
968 return ("$basecom -K -", $l, undef);
969 };
970 }
971
972 # update check
973 if ($vcheck && !length($status)) {
974 $vs = &updatecheck(0);
975 } else {
976 $vs =
977 "-- no version check performed (use /vcheck, or -vcheck to check on startup)\n"
978 unless ($script || $status);
979 }
980 print $stdout $vs; # and then again when client starts up
981
982 ## make sure we have all the authentication pieces we need for the
983 ## chosen method (authtoken handles this for Basic Auth;
984 ## this is where we validate OAuth)
985
986 # if we use OAuth, then don't use any Basic Auth credentials we gave
987 # unless we specifically say -authtype=basic
988 if ($authtype eq 'oauth' && length($user)) {
989 print "** warning: -user is ignored when -authtype=oauth (default)\n";
990 $user = undef;
991 }
992 $whoami = (split(/\:/, $user, 2))[0] unless ($anonymous || !length($user));
993
994 # yes, this is plaintext. obfuscation would be ludicrously easy to crack,
995 # and there is no way to hide them effectively or fully in a Perl script.
996 # so be a good neighbour and leave this the fark alone, okay? stealing
997 # credentials is mean and inconvenient to users. this is blessed by
998 # arrangement with Twitter. don't be a d*ck. thanks for your cooperation.
999 $oauthkey = (!length($oauthkey) || $oauthkey eq 'X') ?
1000 "wmS2Z01t6uHq3sVV1JL4DmZLp" : $oauthkey;
1001 $oauthsecret = (!length($oauthsecret) || $oauthsecret eq 'X') ?
1002 "838jD95T6hPUm6MoBwq6SHAvL9oNoPV6acPXi8Ee8Vj3Mcj0GR" : $oauthsecret;
1003
1004 unless ($anonymous) {
1005 # if we are using Basic Auth, ignore any user token we may have in
1006 # our keyfile
1007 if ($authtype eq 'basic') {
1008 $tokenkey = undef;
1009 $tokensecret = undef;
1010 }
1011 # but if we are using OAuth, we can request one, unless we are in script
1012 elsif ($authtype eq 'oauth' && (!length($keyf) || $oauthwizard)) {
1013 if (length($oauthkey) && length($oauthsecret) &&
1014 !length($tokenkey) && !length($tokensecret)) {
1015 # we have a key, we don't have the user token
1016 # but we can't get that with -script
1017 if ($script) {
1018 print $streamout <<"EOF";
1019 AUTHENTICATION FAILURE
1020 YOU NEED TO GET AN OAuth KEY, or use -authtype=basic
1021 (run oysttyer without -script or -runcommand for help)
1022 EOF
1023 exit;
1024 }
1025 # run the wizard, which writes a keyfile for us
1026 $keyf ||= $attempted_keyf;
1027 print $stdout <<"EOF";
1028
1029 +------------------------------------------------------------------------------+
1030 || WELCOME TO oysttyer: Authorize oysttyer by signing into Twitter with OAuth ||
1031 +------------------------------------------------------------------------------+
1032 Looks like you're starting oysttyer for the first time, and/or creating a
1033 keyfile. Welcome to the most user-hostile, highly obfuscated, spaghetti code
1034 infested and obscenely obscure Twitter client that's out there. You'll love it.
1035
1036 oysttyer generates a keyfile that contains credentials for you, including your
1037 access tokens. This needs to be done JUST ONCE. You can take this keyfile with
1038 you to other systems. If you revoke oysttyer's access, you must remove the
1039 keyfile and start again with a new token. You need to do this once per account
1040 you use with oysttyer; only one account token can be stored per keyfile. If you
1041 have multiple accounts, use -keyf=... to specify different keyfiles. KEEP THESE
1042 FILES SECRET.
1043
1044 ** This wizard will overwrite $keyf
1045 Press RETURN/ENTER to continue or CTRL-C NOW! to abort.
1046 EOF
1047 $j = <STDIN>;
1048 print $stdout "\nRequest from $oauthurl ...";
1049 ($tokenkey, $tokensecret) = &tryhardfortoken($oauthurl,
1050 "oauth_callback=oob");
1051 $mytoken = $tokenkey;
1052 $mytokensecret = $tokensecret; # needs to be in both places
1053 # kludge in case user does not specify SSL and this is
1054 # Twitter: we know Twitter supports SSL
1055 ($oauthauthurl =~ /twitter/) &&
1056 ($oauthauthurl =~ s/^http:/https:/);
1057 print $stdout <<"EOF";
1058
1059 1. Visit, in your browser, ALL ON ONE LINE,
1060
1061 ${oauthauthurl}?oauth_token=$mytoken
1062
1063 2. If you are not already signed in, fill in your username and password.
1064
1065 3. Verify that oysttyer is the requesting application, and that its permissions
1066 are as you expect (read your timeline, see who you follow and follow new
1067 people, update your profile, post tweets on your behalf and access your
1068 direct messages). IF THIS IS NOT CORRECT, PRESS CTRL-C NOW!
1069
1070 4. Click Authorize app.
1071
1072 5. A PIN will appear. Enter it below.
1073
1074 EOF
1075 $j = '';
1076 while(!(0+$j)) {
1077 print $stdout "Enter PIN> ";
1078 chomp($j = <STDIN>);
1079 }
1080 print $stdout "\nRequest from $oauthaccurl ...";
1081 ($tokenkey, $tokensecret) = &tryhardfortoken($oauthaccurl,
1082 "oauth_verifier=$j");
1083
1084 $oauthkey = "X";
1085 $oauthsecret = "X";
1086 open(W, ">$keyf") ||
1087 die("Failed to write keyfile $keyf: $!\n");
1088 print W <<"EOF";
1089 ck=${oauthkey}&cs=${oauthsecret}&at=${tokenkey}&ats=${tokensecret}
1090 EOF
1091 close(W);
1092 chmod(0600, $keyf) || print $stdout
1093 "Warning: could not change permissions on $keyf : $!\n";
1094 print $stdout <<"EOF";
1095 Written keyfile $keyf
1096
1097 Now, restart oysttyer to use this keyfile.
1098 (To choose between multiple keyfiles other than the default .oysttyerkey,
1099 tell oysttyer where the key is using -keyf=... .)
1100
1101 EOF
1102 exit;
1103 }
1104 # if we get three of the four, this must have been command line
1105 if (length($oauthkey) && length($oauthsecret) &&
1106 (!length($tokenkey) || !length($tokensecret))) {
1107 my $error = undef;
1108 my $k;
1109 foreach $k (qw(oauthkey oauthsecret tokenkey tokensecret)) {
1110 $error .= "** you need to specify -$k\n"
1111 if (!length($$k));
1112 }
1113 if (length($error)) {
1114 print $streamout <<"EOF";
1115
1116 you are missing portions of the OAuth sequence. either create a keyfile
1117 and point to it with -keyf=... or add these missing pieces:
1118 $error
1119 then restart oysttyer, or use -authtype=basic.
1120 EOF
1121 exit;
1122 }
1123 }
1124 } elsif ($retoke && length($keyf)) {
1125 # start the "re-toke" wizard to convert DM-less cloned app keys.
1126 # dup STDIN for systems that can only "close" it once
1127 open(STDIN2, "<&STDIN") || die("couldn't dup STDIN: $!\n");
1128 print $stdout <<"EOF";
1129
1130 +-------------------------------------------------------------------------+
1131 || The Re-Toke Wizard: Generate a new oysttyer keyfile for your app/token ||
1132 +-------------------------------------------------------------------------+
1133 Twitter is requiring tokens to now have specific permissions to READ
1134 direct messages. This will be enforced by 1 July 2011. If you find you are
1135 unable to READ direct messages, you will need this wizard. DO NOT use this
1136 wizard if you are NOT using a cloned app key (1.2 and on) -- use -oauthwizard.
1137
1138 This wizard will create a new keyfile for you from your app/user keys/tokens.
1139 You do NOT need this wizard if you are using oysttyer for a purpose that does
1140 not require direct message access. For example, if oysttyer is acting as
1141 your command line posting agent, or you are only using it to read your
1142 timeline, you do NOT need a new token. You also do not need a new token to
1143 SEND a direct message, only to READ ones this account has received.
1144
1145 You SHOULD NOT need this wizard if your app key was cloned after 1 June 2011.
1146 However, you can still use it if you experience this specific issue with DMs,
1147 or need to rebuild your keyfile for any other reason.
1148
1149 ** This wizard will overwrite the key at $keyf
1150 ** To change this, restart oysttyer with -retoke -keyf=/path/to/keyfile
1151 Press RETURN/ENTER to continue, or CTRL-C NOW! to abort.
1152 EOF
1153
1154 $j = <STDIN>;
1155 print $stdout <<"EOF";
1156
1157 First: let's get your API key, consumer key and consumer secret.
1158 Start your browser.
1159 1. Log into https://twitter.com/ with your desired account.
1160 2. Go to this URL. You must be logged into Twitter FIRST!
1161
1162 https://dev.twitter.com/apps
1163
1164 3. Click the oysttyer cloned app key you need to regenerate or upgrade.
1165 4. Click Edit Application Settings.
1166 5. Make sure Read, Write & Private Message is selected, and click the
1167 "Save application" button.
1168 6. Select All (CTRL/Command-A) on the next screen, copy (CTRL/Command-C) it,
1169 and paste (CTRL/Command-V) it into this window. (You can also cut and
1170 paste a smaller section if I can't understand your browser's layout.)
1171 7. Press ENTER/RETURN and CTRL-D when you have pasted the window contents.
1172 EOF
1173
1174 $q = $/;
1175 PASTE1LOOP: for(;;) {
1176 print $stdout <<"EOF";
1177
1178 -- Press ENTER and CTRL-D AFTER you have pasted the window contents! ---------
1179 Go ahead:
1180 EOF
1181 undef $/;
1182 $j = <STDIN2>;
1183 print $stdout <<"EOF";
1184
1185 -- EOF -----------------------------------------------------------------------
1186 Processing ...
1187
1188 EOF
1189 $j =~ s/[\r\n]/ /sg;
1190
1191 # process this. as a checksum, API key should == consumer key.
1192 $ck = '';
1193 $cs = '';
1194 ($j =~ /Consumer key\s+([-a-zA-Z0-9_]{10,})\s+/) && ($ck = $1);
1195 ($j =~ /Consumer secret\s+([-a-zA-Z0-9_]{10,})\s+/) &&
1196 ($cs = $1);
1197
1198 if (!length($ck) || !length($cs)) {
1199 # escape hatch
1200 print $stdout <<"EOF";
1201 Something's wrong: I could not find your consumer key or consumer
1202 secret in that text. If this was a misfired paste, please restart the wizard.
1203 Otherwise, bug us \@oysttyer or \#oysttyer or https://github.com/oysttyer/oysttyer
1204 Please don't send keys or secrets.
1205
1206 EOF
1207 exit;
1208 }
1209 last PASTE1LOOP;
1210 }
1211 # this part is similar to the retoke.
1212 $oauthkey = $ck;
1213 $oauthsecret = $cs;
1214 print $stdout "\nI'm testing this key to see if it works.\n";
1215 print $stdout "Request from $oauthurl ...";
1216 ($tokenkey, $tokensecret) = &tryhardfortoken($oauthurl,
1217 "oauth_callback=oob");
1218 $mytoken = $tokenkey;
1219 $mytokensecret = $tokensecret;
1220 # kludge in case user does not specify SSL and this is
1221 # Twitter: we know Twitter supports SSL
1222 ($oauthauthurl =~ /twitter/) && ($oauthauthurl =~ s/^http:/https:/);
1223 $/ = $q;
1224 print $stdout <<"EOF";
1225
1226 Okay, your consumer key is ==> $ck
1227 and your consumer secret ==> $cs
1228
1229 IF THIS IS WRONG, PRESS CTRL-C NOW AND RESTART THE WIZARD!
1230
1231 Now we will verify your Imperial battle station is fully operational by
1232 signing in with OAuth.
1233
1234 1. Visit, in your browser, ALL ON ONE LINE (you should still be logged in),
1235
1236 ${oauthauthurl}?oauth_token=$mytoken
1237
1238 2. Verify that your app is the requesting application, and that its permissions
1239 are as you expect (read your timeline, see who you follow and follow new
1240 people, update your profile, post tweets on your behalf and access your
1241 direct messages). IF THIS IS NOT CORRECT, PRESS CTRL-C NOW!
1242
1243 3. Click Authorize app.
1244
1245 4. A PIN will appear. Enter it below.
1246
1247 EOF
1248 print $stdout "Enter PIN> ";
1249 chomp($j = <STDIN>);
1250 print $stdout "\nRequest from $oauthaccurl ...";
1251 ($at, $ats) = &tryhardfortoken($oauthaccurl, "oauth_verifier=$j");
1252
1253 print $stdout <<"EOF";
1254
1255 Consumer key =========> $ck
1256 Consumer secret ======> $cs
1257 Access token =========> $at
1258 Access token secret ==> $ats
1259
1260 EOF
1261 open(W, ">$keyf") || (print $stdout ("Unable to write to $keyf: $!\n"),
1262 exit);
1263 print W "ck=$ck&cs=$cs&at=$at&ats=$ats\n";
1264 close(W);
1265 chmod(0600, $keyf) || print $stdout
1266 "Warning: could not change permissions on $keyf : $!\n";
1267 print $stdout "Keys written to regenerated keyfile $keyf\n";
1268 print $stdout "Now restart oysttyer.\n";
1269 exit;
1270 }
1271
1272 # now, get a token (either from Basic Auth, the keyfile or OAuth)
1273 ($mytoken, $mytokensecret) = &authtoken;
1274 } # unless anonymous
1275
1276 # if we are testing the stream, this is where we split
1277 if ($streamtest) {
1278 print $stdout ">>> STREAMING CONNECT TEST <<< (kill process to end)\n";
1279 &start_streaming; } # this never returns in this mode
1280
1281 # initial login tests and command line controls
1282 if ($statusurl) {
1283 $shorstatusturl = &urlshorten($statusurl);
1284 $status = ((length($status)) ? "$status " : "") . $shorstatusturl;
1285 }
1286 $phase = 0;
1287 $didhold = $hold;
1288 $hold = -1 if ($hold == 1 && !$script);
1289 $credentials = '';
1290 $status = pack("U0C*", unpack("C*", $status))
1291 unless ($seven || !length($status) || $LANG =~ /8859/); # kludgy also
1292 if ($status eq '-') {
1293 chomp(@status = <STDIN>);
1294 $status = join("\n", @status);
1295 }
1296 for(;;) {
1297 $rv = 0;
1298 die(
1299 "sorry, you can't tweet anonymously. use an authenticated username.\n")
1300 if ($anonymous && length($status));
1301 die(
1302 "sorry, status too long: reduce by @{[ &length_tco($status)-$linelength ]} chars, ".
1303 "or use -autosplit={word,char,cut}.\n")
1304 if (&length_tco($status) > $linelength && !$autosplit);
1305 ($status, $next) = &csplit($status, $autosplit)
1306 if (!length($next));
1307 if ($autosplit eq 'cut' && length($next)) {
1308 print "-- warning: input autotrimmed to $linelength bytes\n";
1309 $next = "";
1310 }
1311 if (!$anonymous && !length($whoami) && !length($status)) {
1312 # we must be using OAuth tokens. we'll need
1313 # to get our screen name from Twitter. we DON'T need this
1314 # if we're just posting with -status.
1315 print "(checking credentials) "; $data =
1316 $credentials = &backticks($baseagent, '/dev/null', undef,
1317 $credurl, undef, $anonymous, @wind);
1318 $rv = $? || &is_fail_whale($data) || &is_json_error($data);
1319 }
1320 if (!$rv && length($status) && $phase) {
1321 print "post attempt "; $rv = &updatest($status, 0);
1322 } else {
1323 # no longer a way to test anonymous logins
1324 unless ($rv || $anonymous) {
1325 print "test-login ";
1326 $data = &backticks($baseagent, '/dev/null', undef,
1327 $url, undef, $anonymous, @wind);
1328 $rv = $?;
1329 }
1330 }
1331 if ($rv || &is_fail_whale($data) || &is_json_error($data)) {
1332 if ($rv == 96 || $rv == 97 || $rv == 99) {
1333 print "post CANCELLED!\n";
1334 exit(1);
1335 } elsif (&is_fail_whale($data)) {
1336 print "FAILED -- Fail Whale detected\n";
1337 } elsif ($x = &is_json_error($data)) {
1338 print "FAILED!\n*** server reports: \"$x\"\n";
1339 print "check your password or configuration.\n";
1340 } else {
1341 $x = $rv >> 8;
1342 print
1343 "FAILED. ($x) bad password, login or URL? server down?\n";
1344 }
1345 print "access failure on: ";
1346 print (($phase) ? $update : $url);
1347 print "\n";
1348 print
1349 "--- data received ($hold) ---\n$data\n--- data received ($hold) ---\n"
1350 if ($superverbose);
1351 if ($hold && --$hold) {
1352 print
1353 "trying again in 1 minute, or kill process now.\n\n";
1354 sleep 60;
1355 next;
1356 }
1357 if ($didhold) {
1358 print "giving up after $didhold tries.\n";
1359 } else {
1360 print
1361 "to automatically wait for a connect, use -hold.\n";
1362 }
1363 exit(1);
1364 }
1365 if ($status && !$phase) {
1366 print "SUCCEEDED!\n";
1367 $phase++;
1368 next;
1369 }
1370 if (length($next)) {
1371 print "SUCCEEDED!\n(autosplit) ";
1372 $status = $next;
1373 $next = "";
1374 next;
1375 }
1376 last;
1377 }
1378 print "SUCCEEDED!\n";
1379 exit(0) if (length($status));
1380 &sigify(sub { ; }, qw(USR1 PWR XCPU));
1381 &sigify(sub { $background_is_ready++ }, qw(USR2 SYS UNUSED XFSZ));
1382 if (length($credentials)) {
1383 print "-- processing credentials: ";
1384 $my_json_ref = &parsejson($credentials);
1385 $whoami = lc($my_json_ref->{'screen_name'});
1386 if (!length($whoami)) {
1387 print "FAILED!\nis your account suspended, or wrong token?\n";
1388 exit;
1389 }
1390 print "logged in as $whoami\n";
1391 $credlog = "-- you are logged in as $whoami\n";
1392 }
1393
1394 #### BOT/DAEMON MODE STARTUP ####
1395
1396 $last_rate_limit = undef;
1397 $rate_limit_left = undef;
1398 $rate_limit_rate = undef;
1399 $rate_limit_next = 0;
1400 $effpause = 0; # for both daemon and background
1401 if ($daemon) {
1402 if (!$pause) {
1403 print $stdout "*** kind of stupid to run daemon with pause=0\n";
1404 exit 1;
1405 }
1406
1407 $lockf ||= "$ENV{'HOME'}/.oysttyerlock";
1408 $lockf = "$ENV{'HOME'}/.oysttyerlock${lockf}" if ($lockf !~ m#/#);
1409
1410 if ( -f $lockf) {
1411 unless (open(L, "<$lockf")) {
1412 print $stdout "*** unable to open existing lock: $!\n";
1413 exit 1;
1414 }
1415 while (<L>) {
1416 chomp();
1417 next unless (/^\d+$/);
1418 if (kill 0, $_) {
1419 print $stdout "*** instance already running: $_\n";
1420 exit 1;
1421 }
1422 }
1423 unless (unlink($lockf)) {
1424 print $stdout "*** unable to remove stale lock: $!\n";
1425 exit 1;
1426 }
1427 }
1428
1429 unless (open(L, ">$lockf")) {
1430 print $stdout "*** unable to create lock: $lockf: $!\n";
1431 exit 1;
1432 }
1433
1434 if ($child = fork()) {
1435 unless (print L "$child\n") {
1436 print $stdout "*** unable to write lock: $!\n";
1437 kill 15, $child;
1438 exit 1;
1439 }
1440 unless (close(L)) {
1441 print $stdout "*** unable to close lock: $!\n";
1442 kill 15, $child;
1443 }
1444 print $stdout "*** detached daemon released. pid = $child\n";
1445 kill 15, $$;
1446 exit 0;
1447 } elsif (!defined($child)) {
1448 print $stdout "*** fork() failed: $!\n";
1449 exit 1;
1450 } else {
1451 $bufferpid = 0;
1452 if ($dostream) {
1453 &sigify(sub {
1454 kill $SIGHUP, $nursepid if ($nursepid);
1455 kill $SIGHUP, $bufferpid if ($bufferpid);
1456 kill 9, $curlpid if ($curlpid);
1457 sleep 1;
1458 # send myself a shutdown
1459 kill 9, $nursepid if ($nursepid);
1460 kill 9, $bufferpid if ($bufferpid);
1461 kill 9, $curlpid if ($curlpid);
1462 &rmlock;
1463 kill 9, $$;
1464 }, qw(TERM HUP PIPE));
1465 &sigify("IGNORE", qw(INT));
1466 $bufferpid = &start_streaming;
1467 $rin = '';
1468 vec($rin, fileno(STBUF), 1) = 1;
1469 } else {
1470 &sigify(sub {
1471 &rmlock;
1472 kill 9, $$;
1473 }, qw(TERM HUP PIPE));
1474 }
1475 $parent = 0;
1476 $dmcount = 1 if ($dmpause); # force fetch
1477 $is_background = 1;
1478 DAEMONLOOP: for(;;) {
1479 my $snooze;
1480 my $nfound;
1481 my $wake;
1482
1483 &$heartbeat;
1484 &update_effpause;
1485 &refresh(0);
1486 $dont_refresh_first_time = 0;
1487 if ($dmpause) {
1488 if (!--$dmcount) {
1489 &dmrefresh(0);
1490 $dmcount = $dmpause;
1491 }
1492 }
1493 # service events on the streaming socket, if
1494 # we have one.
1495 $snooze = ($effpause || 0+$pause || 60);
1496 $wake = time() + $snooze;
1497 if (!$bufferpid) {
1498 sleep $snooze;
1499 } else {
1500 my $read_failure = 0;
1501 SLEEP_AGAIN: for(;;) {
1502 $nfound = select($rout = $rin,
1503 undef, undef, $snooze);
1504 if ($nfound &&
1505 vec($rout, fileno(STBUF), 1)==1) {
1506 my $buf = '';
1507 my $rbuf = '';
1508 my $len;
1509
1510 read(STBUF, $buf, 1);
1511 if (!length($buf)) {
1512 $read_failure++;
1513 # a stuck ready FH says
1514 # our buffer is dead;
1515 # see MONITOR: below.
1516 if ($read_failure>100){
1517 print $stdout "*** unrecoverable failure of buffer process, aborting\n";
1518 exit;
1519 }
1520 next SLEEP_AGAIN;
1521 }
1522 $read_failure = 0;
1523 if ($buf !~ /^[0-9a-fA-F]+$/) {
1524 print $stdout
1525 "-- warning: bogus character(s) ".unpack("H*", $buf)."\n"
1526 if ($superverbose);
1527 next SLEEP_AGAIN;
1528 }
1529 while (length($buf) < 8) {
1530 # don't read 8 -- read 1. that means we can
1531 # skip trailing garbage without a window.
1532 read(STBUF,$rbuf,1);
1533 if ($rbuf =~ /[0-9a-fA-F]/) {
1534 $buf .= $rbuf;
1535 } else {
1536 print $stdout
1537 "-- warning: bogus character(s) ".unpack("H*", $rbuf)."\n"
1538 if ($superverbose);
1539 $buf = ''
1540 if(length($rbuf));
1541 }
1542 }
1543 print $stdout "-- length packet: $buf\n"
1544 if ($superverbose);
1545 $len = hex($buf);
1546 $buf = '';
1547 while (length($buf) < $len) {
1548 read(STBUF, $rbuf,
1549 ($len-length($buf)));
1550 $buf .= $rbuf;
1551 }
1552 &streamevents(
1553 &parsejson($buf) );
1554 $snooze = $wake - time();
1555 next SLEEP_AGAIN if
1556 ($snooze > 0);
1557 }
1558 last SLEEP_AGAIN;
1559 }
1560 }
1561 }
1562 }
1563 die("uncaught fork() exception\n");
1564 }
1565
1566 #### INTERACTIVE MODE and CONSOLE STARTUP ####
1567
1568 unless ($simplestart) {
1569 print <<"EOF";
1570
1571 ###################################################### +oo=========oo+
1572 ${EM}oysttyer ${oysttyer_VERSION}.${padded_patch_version} (c)2017 oysttyer organisation
1573 (c)2007-2012 cameron kaiser${OFF}
1574 EOF
1575 $e = <<'EOF';
1576 all rights reserved. .#*^#=.
1577 https://oysttyer.github.io/ %'.,`.#`
1578 ;',. ./#`
1579 freeware under the floodgap free software license. ({.`,` #/
1580 http://www.floodgap.com/software/ffsl/ `& ,` %,~=*'"*=~=-.,
1581 \`=_/'.`` - `'. *\.
1582 tweet us http://twitter.com/oysttyer (%. - - Ë‹-. `&
1583 `&` ~ @ . #
1584 ###################################################### `\`. ` .....ËŠ %'
1585 # `^~._.,,,.-+=~*'
1586 # when ready, hit RETURN/ENTER for a prompt.
1587 # type /help for commands or /quit to quit.
1588 # starting background monitoring process.
1589 # /.\ | ||_ |_ |_ | | /_\|'`
1590 # \_/ \_| _|'|_'|_ \_| \_ |
1591 # | |
1592 EOF
1593 $e =~ s/\$\{([A-Z]+)\}/${$1}/eg; print $stdout $e;
1594 } else {
1595 print <<"EOF";
1596 oysttyer ${oysttyer_VERSION}.${padded_patch_version} (c)2017 oysttyer organisation
1597 (c)2007-2012 cameron kaiser
1598 all rights reserved. freeware under the floodgap free software license.
1599 http://www.floodgap.com/software/ffsl/
1600
1601 tweet us http://twitter.com/oysttyer
1602 type /help for commands or /quit to quit.
1603 starting background monitoring process.
1604
1605 EOF
1606 }
1607 if ($superverbose) {
1608 print $stdout "-- OMGSUPERVERBOSITYSPAM enabled.\n\n";
1609 } else {
1610 print $stdout "-- verbosity enabled.\n\n" if ($verbose);
1611 }
1612 sleep 3 unless ($silent);
1613
1614 # these three functions are outside of the usual API assertions for clarity.
1615 # they represent the main loop, which by default is the interactive console.
1616 # the main loop can be redefined.
1617
1618 #configure promptprefix
1619 if ($showusername) {
1620 $promptprefix = $whoami ;
1621 } else {
1622 $promptprefix = "oysttyer";
1623 }
1624
1625 sub defaultprompt {
1626 my $rv = ($noprompt) ? "" : "$promptprefix> ";
1627 my $rvl = ($noprompt) ? 0 : 9;
1628 return ($rv, $rvl) if (shift);
1629 $wrapseq = 0;
1630 print $stdout "${CCprompt}$rv${OFF}" unless ($termrl);
1631 }
1632 sub defaultaddaction { return 0; }
1633 sub defaultmain {
1634 if (length($runcommand)) {
1635 &prinput($runcommand);
1636 &sync_n_quit;
1637 }
1638 @history = ();
1639 print C "rsga---------------\n";
1640 $dont_use_counter = $nocounter;
1641 eval '$termrl->hook_no_counter';
1642 $tco_sub = sub { return &main::fastturntotco(shift); };
1643 eval '$termrl->hook_no_tco';
1644 if ($termrl) {
1645 while(defined ($_ = $termrl->readline((&$prompt(1))[0]))) {
1646 kill $SIGUSR1, $child; # suppress output
1647 $rv = &prinput($_);
1648 kill $SIGUSR2, $child; # resume output
1649 last if ($rv < 0);
1650 &sync_console unless (!$rv || !$synch);
1651 if ($dont_use_counter ne $nocounter) {
1652 # only if we have to -- this is expensive
1653 $dont_use_counter = $nocounter;
1654 eval '$termrl->hook_no_counter'
1655 }
1656 }
1657 } else {
1658 &$prompt;
1659 while(<>) { #not stdin so we can read from script files
1660 kill $SIGUSR1, $child; # suppress output
1661 $rv = &prinput(&uforcemulti($_));
1662 kill $SIGUSR2, $child; # resume output
1663 last if ($rv < 0);
1664 &sync_console unless (!$rv || !$synch);
1665 &$prompt;
1666 }
1667 &sync_n_quit if ($script);
1668 }
1669 }
1670
1671 # SIGPIPE in particular must be trapped in case someone kills the background
1672 # or, in streaming mode, buffer processes. we can't recover from that.
1673 # the streamer MUST have been initialized before we start these signal
1674 # handlers, or the streamer will try to run them too. eeek!
1675 #
1676 # DO NOT trap SIGCHLD: we generate child processes that die normally.
1677 &sigify(\&end_me, qw(PIPE INT));
1678 &sigify(\&repaint, qw(USR1 PWR XCPU));
1679 sub sigify {
1680 # this routine abstracts setting signals to a subroutine reference.
1681 # check and see if we have to use POSIX.pm (Perl 5.14+) or we can
1682 # still use $SIG for proper signalling. We prefer the latter, but
1683 # must support the former.
1684 my $subref = shift;
1685 my $k;
1686
1687 if ($signals_use_posix) {
1688 my @w;
1689 my $sigaction = POSIX::SigAction->new($subref);
1690 while ($k = shift) {
1691 my $e = &posix_signal_of($k);
1692 # some signals may not exist on all systems.
1693 next if (!(0+$e));
1694 POSIX::sigaction($e, $sigaction)
1695 || die("sigaction failure: $! $@\n");
1696 }
1697 } else {
1698 while ($k = shift) { $SIG{$k} = $subref; }
1699 }
1700 }
1701 sub posix_signal_of {
1702 die("never call posix_signal_of if signals_use_posix is false\n")
1703 if (!$signals_use_posix);
1704
1705 # this assumes that POSIX::SIG* returns a scalar int value.
1706 # not all signals exist on all systems. this ensures zeroes are
1707 # returned for locally bogus ones.
1708 return 0+(eval("return POSIX::SIG".shift));
1709 }
1710
1711 sub send_repaint {
1712 unless ($wrapseq){
1713 return;
1714 }
1715 $wrapseq = 0;
1716 return if ($daemon);
1717 if ($child) {
1718 # we are the parent, call our repaint
1719 &repaint;
1720 } else {
1721 # we are not the parent, call the parent to repaint itself
1722 kill $SIGUSR1, $parent; # send SIGUSR1
1723 }
1724 }
1725 sub repaint {
1726 # try to speed this up, since we do it a lot.
1727 $wrapseq = 0;
1728 return &$repaintcache if ($repaintcache) ;
1729
1730 # cache our repaint function (no-op or redisplay)
1731 $repaintcache = sub { ; }; # no-op
1732 return unless ($termrl &&
1733 ($termrl->Features()->{'canRepaint'} || $readlinerepaint));
1734 return if ($daemon);
1735 $termrl->redisplay; $repaintcache = sub { $termrl->redisplay; };
1736 }
1737 sub send_removereadline {
1738 # this just stubs into its own removereadline
1739 return &$removereadlinecache if ($removereadlinecache);
1740
1741 $removereadlinecache = sub { ; };
1742 return unless ($termrl && $termrl->Features()->{'canRemoveReadline'});
1743 return if ($daemon);
1744 $termrl->removereadline;
1745 $removereadlinecache = sub { $termrl->removereadline; };
1746 }
1747
1748 # start the background process
1749 # this has to be last or the background process can't see the full API
1750 if ($child = open(C, "|-")) {
1751 close(P);
1752 } else {
1753 close(W);
1754 goto MONITOR;
1755 }
1756 eval'$termrl->hook_background_control' if ($termrl);
1757 select(C); $|++; select($stdout);
1758
1759 # handshake for synchronicity mode, if we want it.
1760 if ($synch) {
1761 # we will get two replies for this.
1762 print C "synm---------------\n";
1763 &thump;
1764 # the second will be cleared by the console
1765 }
1766
1767 # wait for background to become ready
1768 sleep 1 while (!$background_is_ready);
1769
1770 # start the
1771 &$main;
1772 # loop until we quit and then we'll
1773 &sync_n_quit if ($script);
1774 # else
1775 exit;
1776
1777 #### command processor ####
1778
1779 sub prinput {
1780 my $i;
1781 local($_) = shift; # bleh
1782
1783 # validate this string if we are in UTF-8 mode
1784 unless ($seven) {
1785 $probe = $_;
1786 &$utf8_encode($probe);
1787 die("utf8 doesn't work right in this perl. run with -seven.\n")
1788 if (&ulength($probe) < length($_));
1789 # should be at least as big
1790 if ($probe =~ /($badutf8)/) {
1791 print $stdout "*** invalid UTF-8: partial delete of a wide character?\n";
1792 print $stdout "*** ignoring this string\n";
1793 return 0;
1794 }
1795 }
1796
1797 $in_reply_to = 0;
1798 $quoted_status_url = undef;
1799 chomp;
1800 $_ = &$precommand($_);
1801 s/^\s+//;
1802 s/\s+$//;
1803 my $cfc = 0;
1804 $cfc++ while (s/\033\[[0-9]?[ABCD]// || s/.[\177]// || s/.[\010]//
1805 || s/[\000-\037\177]//);
1806 if ($cfc) {
1807 $history[0] = $_;
1808 print $stdout "*** filtered control characters; now \"$_\"\n";
1809 print $stdout "*** use %% for truncated version, or append to %%.\n";
1810 return 0;
1811 }
1812
1813 if (/^$/) {
1814 return 1;
1815 }
1816
1817 if (!$slowpost && !$verify && # we assume you know what you're doing!
1818 ($_ eq 'h' || $_ eq 'help' || $_ eq 'quit' || $_ eq 'q' ||
1819 /^oysttyer>/ || $_ eq 'ls' || $_ eq '?' ||
1820 m#^help /# || $_ eq 'exit')) {
1821
1822 &add_history($_);
1823 unless ($_ eq 'exit' || /^oysttyer>/ || $_ eq 'ls') {
1824 print $stdout "*** did you mean /$_ ?\n";
1825 print $stdout
1826 "*** to send this as a command, type /%%\n";
1827 } else {
1828 print $stdout
1829 "*** did you really mean to tweet \"$_\"?\n";
1830 }
1831 print $stdout "*** to tweet it anyway, type %%\n";
1832 return 0;
1833 }
1834
1835 if (/^\%(\%|-\d+):p$/) {
1836 my $x = $1;
1837 if ($x eq '%') {
1838 print $stdout "=> \"$history[0]\"\n";
1839 } else {
1840 $x += 0;
1841 if (!$x || $x < -(scalar(@history))) {
1842 print $stdout "*** illegal index\n";
1843 } else {
1844 print $stdout "=> \"$history[-($x + 1)]\"\n";
1845 }
1846 }
1847 return 0;
1848 }
1849
1850 # handle history substitution (including /%%, %%--, %%*, etc.)
1851 $i = 0; # flag
1852
1853 if (/^\%(\%|-\d+)(--|-\d+|\*)?/) {
1854 ($i, $proband, $r, $s) = &sub_helper($1, $2, $_);
1855 return 0 if (!$i);
1856
1857 $s = quotemeta($s);
1858 s/^\%${r}${s}/$proband/;
1859 }
1860 if (/[^\\]\%(\%|-\d+)(--|-\d+|\*)?$/) {
1861 ($i, $proband, $r, $s) = &sub_helper($1, $2, $_);
1862 return 0 if (!$i);
1863
1864 $s = quotemeta($s);
1865 s/\%${r}${s}$/$proband/;
1866 }
1867 # handle variables second, in case they got in history somehow ...
1868 $i = 1 if (s/^\%URL\%/$urlshort/ || s/\%URL\%$/$urlshort/);
1869 $i = 1 if (s/^\%RT\%/$retweet/ || s/\%RT\%$/$retweet/);
1870
1871 # and escaped history
1872 s/^\\\%/%/;
1873
1874 if ($i) {
1875 print $stdout "(expanded to \"$_\")\n" ;
1876 $in_reply_to = $expected_tweet_ref->{'id_str'} || 0
1877 if (defined $expected_tweet_ref &&
1878 ref($expected_tweet_ref) eq 'HASH');
1879 } else {
1880 $expected_tweet_ref = undef;
1881 }
1882
1883 return 0 unless length; # actually possible to happen
1884 # with control char filters and history.
1885
1886 &add_history($_);
1887 $shadow_history = $_;
1888
1889 # handle history display
1890 if ($_ eq '/history' || $_ eq '/h') {
1891 for ($i = scalar(@history); $i >= 1; $i--) {
1892 print $stdout "\t$i\t$history[($i-1)]\n";
1893 }
1894 return 0;
1895 }
1896
1897 my $slash_first = ($_ =~ m#^/#);
1898
1899 return -1 if ($_ eq '/quit' || $_ eq '/q' || $_ eq '/bye' ||
1900 $_ eq '/exit');
1901
1902 return 0 if (scalar(&$addaction($_)));
1903
1904 # add commands here
1905
1906 # dumper
1907 if (m#^/du(mp)? ([zZ]?[a-zA-Z]?[0-9]+)$#) {
1908 my $code = lc($2);
1909 unless ($code =~ /^d[0-9][0-9]+$/) { # this is a DM.
1910 my $tweet = &get_tweet($code);
1911 my $k;
1912 my $sn;
1913 my $id;
1914 my @superfields = (
1915 [ "user", "screen_name" ], # must always be first
1916 [ "extended_tweet", "full_text" ],
1917 [ "retweeted_status", "id_str" ],
1918 [ "retweeted_status", "full_text" ],
1919 [ "retweeted_status", "text" ],
1920 [ "user", "geo_enabled" ],
1921 [ "place", "id" ],
1922 [ "place", "country_code" ],
1923 [ "place", "full_name" ],
1924 [ "place", "place_type" ],
1925 [ "tag", "type" ],
1926 [ "tag", "payload" ],
1927 );
1928 my $superfield;
1929
1930 if (!defined($tweet)) {
1931 print $stdout "-- no such tweet (yet?): $code\n";
1932 return 0;
1933 }
1934
1935 foreach $superfield (@superfields) {
1936 my $sfn = join('->', @{ $superfield });
1937 my $sfk = "{'" . join("'}->{'", @{ $superfield }) .
1938 "'}";
1939 my $sfv;
1940 eval "\$sfv = &descape(\$tweet->$sfk);";
1941 print $stdout
1942 substr("$sfn ", 0, 25).
1943 " $sfv\n";
1944 $sn = $sfv if (!length($sn) && length($sfv));
1945 }
1946 # geo is special
1947 print $stdout "geo->coordinates (" .
1948 join(', ', @{ $tweet->{'geo'}->{'coordinates'} })
1949 . ")\n";
1950 foreach $k (sort keys %{ $tweet }) {
1951 next if (ref($tweet->{$k}));
1952 print $stdout
1953 substr("$k ", 0, 25) .
1954 " " . &descape($tweet->{$k}) . "\n";
1955 }
1956 # include a URL to the tweet per @augmentedfourth
1957 $urlshort =
1958 "${http_proto}://twitter.com/$sn/statuses/$tweet->{'id_str'}";
1959 print $stdout
1960 "-- %URL% is now $urlshort (/short to shorten)\n";
1961 return 0;
1962 } # if dxxxx, fall through to the below.
1963 }
1964
1965 if (m#^/du(mp)? ([dD][a-zA-Z]?[0-9]+)$#) {
1966 my $code = lc($2);
1967 my $dm = &get_dm($code);
1968 my $k;
1969 my $sn;
1970 my $id;
1971 my @superfields = (
1972 [ "sender", "screen_name" ], # must always be first
1973 );
1974
1975 if (!defined($dm)) {
1976 print $stdout "-- no such DM (yet?): $code\n";
1977 return 0;
1978 }
1979
1980 foreach $superfield (@superfields) {
1981 my $sfn = join('->', @{ $superfield });
1982 my $sfk = "{'" . join("'}->{'", @{ $superfield }) .
1983 "'}";
1984 my $sfv;
1985 eval "\$sfv = &descape(\$dm->$sfk);";
1986 print $stdout
1987 substr("$sfn ", 0, 25).
1988 " $sfv\n";
1989 $sn = $sfv if (!length($sn) && length($sfv));
1990 }
1991
1992 foreach $k (sort keys %{ $dm }) {
1993 next if (ref($dm->{$k}));
1994 print $stdout
1995 substr("$k ", 0, 25) .
1996 " " . &descape($dm->{$k}) . "\n";
1997 }
1998 return 0;
1999 }
2000
2001 # evaluator
2002 if (m#^/ev(al)? (.+)$#) {
2003 $k = eval $2;
2004 print $stdout "==> ";
2005 print $streamout "$k $@\n";
2006 return 0;
2007 }
2008
2009 # version check
2010 if (m#^/v(ersion)?check$# || m#^/u(pdate)?check$#) {
2011 print $stdout &updatecheck(1);
2012 return 0;
2013 }
2014
2015 # url shortener routine
2016 if (($_ eq '/sh' || $_ eq '/short') && length($urlshort)) {
2017 $_ = "/short $urlshort";
2018 print $stdout "*** assuming you meant %URL%: $_\n";
2019 # and fall through to ...
2020 }
2021 if (m#^/sh(ort)? (https?|gopher)(://[^ ]+)#) {
2022 my $url = $2 . $3;
2023 my $answer = (&urlshorten($url) || 'FAILED -- %% to retry');
2024 print $stdout "*** shortened to: ";
2025 print $streamout ($answer . "\n");
2026 $urlshort = $answer;
2027 return 0;
2028 }
2029
2030 # getter for internal value settings
2031 if (/^\/r(ate)?l(imit)?$/) {
2032 $_ = '/print rate_limit_rate';
2033 # and fall through to ...
2034 }
2035
2036 if ($_ eq '/p' || $_ eq '/print') {
2037 foreach $key (sort keys %opts_can_set) {
2038 print $stdout "*** $key => $$key\n"
2039 if (!$opts_secret{$key});
2040 }
2041 return 0;
2042 }
2043 if (/^\/p(rint)?\s+([^ ]+)/) {
2044 my $key = $2;
2045 if ($valid{$key} ||
2046 $key eq 'effpause' ||
2047 $key eq 'rate_limit_rate' ||
2048 $key eq 'rate_limit_left') {
2049 my $value = &getvariable($key);
2050 print $stdout "*** ";
2051 print $stdout "(read-only value) "
2052 if (!$opts_can_set{$key});
2053 print $stdout "$key => $value\n";
2054
2055 # I don't see a need for these in &getvariable, so they are
2056 # not currently supported. whine if you disagree.
2057
2058 } elsif ($key eq 'tabcomp') {
2059 if ($termrl) {
2060 &generate_otabcomp;
2061 } else {
2062 print $stdout "*** readline isn't on\n";
2063 }
2064 } elsif ($key eq 'ntabcomp') { # sigh
2065 if ($termrl) {
2066 print $stdout "*** new TAB-comp entries: ";
2067 $did_print = 0;
2068 foreach(keys %readline_completion) {
2069 next if ($original_readline{$_});
2070 $did_print = 1;
2071 print $stdout "$_ ";
2072 }
2073 print $stdout "(none)" if (!$did_print);
2074 print $stdout "\n";
2075 } else {
2076 print $stdout "*** readline isn't on\n";
2077 }
2078
2079 } else {
2080 print "*** not a valid option or setting: $key\n";
2081 }
2082 return 0;
2083 }
2084 if ($_ eq '/verbose' || $_ eq '/ve') {
2085 $verbose ^= 1;
2086 $_ = "/set verbose $verbose";
2087 print $stdout "-- verbosity.\n" if ($verbose);
2088 # and fall through to set
2089 }
2090
2091 # search api integration (originally based on @kellyterryjones',
2092 # @vielmetti's and @br3nda's patches)
2093 if (/^\/se(arch)?\s+(\+\d+\s+)?(.+)\s*$/) {
2094 my $countmaybe = $2;
2095 my $kw = $3;
2096 $countmaybe =~ s/[^\d]//g if (length($countmaybe));
2097 $countmaybe += 0;
2098 $countmaybe ||= $searchhits;
2099 $kw = &url_oauth_sub($kw);
2100 $kw = "q=$kw" if ($kw !~ /^q=/);
2101
2102 my $r = &grabjson("$queryurl?$kw", 0, 0, $countmaybe, {
2103 "type" => "search",
2104 "payload" => $k
2105 }, 1);
2106 if (defined($r) && ref($r) eq 'ARRAY' && scalar(@{ $r })) {
2107 &dt_tdisplay($r, 'search');
2108 } else {
2109 print $stdout "-- sorry, no results were found.\n";
2110 }
2111 &$conclude;
2112 return 0;
2113 }
2114 if ($_ eq '/notrack') { # special case
2115 print $stdout "*** all tracking keywords cancelled\n";
2116 $track = '';
2117 &setvariable('track', $track, 1);
2118 return 0;
2119 }
2120 if (s/^\/troff\s+// && s/\s*// && length) {
2121 # remove it from array, regenerate $track, call tracktags_makearray
2122 # and then sync
2123 my $k;
2124 my $l = '';
2125 my $q = 0;
2126 my %w;
2127 $_ = lc($_);
2128 my (@ptags) = split(/\s+/, $_);
2129
2130 # filter duplicates and merge quoted strings (again)
2131 # but this time we're building up a hash for fast searches
2132 foreach $k (@ptags) {
2133 if ($q && $k =~ /"$/) { # this has to be first
2134 $l .= " $k";
2135 $q = 0;
2136 } elsif ($k =~ /^"/ || $q) {
2137 $l .= (length($l)) ? " $k" : $k;
2138 $q = 1;
2139 next;
2140 } else {
2141 $l = $k;
2142 }
2143 next if ($w{$l}); # ignore silently here
2144 $w{$l} = 1;
2145 $l = '';
2146 }
2147 print $stdout "-- warning: syntax error, missing quote?\n"
2148 if ($q);
2149
2150 # now filter out of @tracktags
2151 @ptags = ();
2152 foreach $k (@tracktags) {
2153 push (@ptags, $k) unless ($w{$k});
2154 }
2155 unless (scalar(@ptags) < scalar(@tracktags)) {
2156 print $stdout "-- sorry, no track terms matched.\n";
2157 print $stdout (length($track) ?
2158 "-- you are tracking: $track\n" :
2159 "-- (maybe because you're not tracking anything?)\n");
2160 return 0;
2161 }
2162 print $stdout "*** ok, filtered @{[ keys(%w) ]}\n";
2163 $track = join(' ', @ptags);
2164 &setvariable('track', $track, 1);
2165 return 0;
2166 }
2167
2168 if (s#^/tre(nds)?\s*##) {
2169 my $t = undef;
2170 my $wwoeid = (length) ? $_ : $woeid;
2171 $wwoeid ||= "1";
2172 my $r = &grabjson("${wtrendurl}?id=${wwoeid}",
2173 0, 0, 0, undef, 1);
2174 my $fr = ($wwoeid && $wwoeid ne '1') ?
2175 " FOR WOEID $wwoeid" : ' GLOBALLY';
2176
2177 if (defined($r) && ref ($r) eq 'ARRAY') {
2178 $t = $r->[0]->{'trends'};
2179 }
2180 if (defined($t) && ref($t) eq 'ARRAY') {
2181 my $i;
2182 my $j;
2183
2184 print $stdout "${EM}<<< TRENDING TOPICS${fr} >>>${OFF}\n";
2185 foreach $j (@{ $t }) {
2186 my $k = &descape($j->{'name'});
2187 my $l = ($k =~ /\sOR\s/) ? $k :
2188 ($k =~ /^"/) ? $k :
2189 ('"' . $k . '"');
2190 print $streamout "/search $l\n";
2191 $k =~ s/\sOR\s/ /g;
2192 $k = '"' . $k . '"' if ($k =~ /\s/
2193 && $k !~ /^"/);
2194 print $streamout "/tron $k\n";
2195 }
2196 print $stdout "${EM}<<< TRENDING TOPICS >>>${OFF}\n";
2197 } else {
2198 print $stdout
2199 "-- sorry, trends not available for WOEID $wwoeid.\n";
2200 }
2201 return 0;
2202 }
2203
2204 # woeid finder based on lat/long
2205 if ($_ eq '/woeids') {
2206 my $max = 10;
2207 if (!$lat && !$long) {
2208 print $stdout
2209 "-- set your location with lat/long first.\n";
2210 return 0;
2211 }
2212 my $r = &grabjson("$atrendurl?lat=$lat&long=$long", 0, 0, 0,
2213 undef, 1);
2214 if (defined($r) && ref($r) eq 'ARRAY') {
2215 my $i;
2216 foreach $i (@{ $r }) {
2217 my $woeid = &descape($i->{'woeid'});
2218 my $nm = &descape($i->{'name'}) . ' (' .
2219 &descape($i->{'countryCode'}) .')';
2220 print $streamout "$nm\n/set woeid $woeid\n";
2221 last unless ($max--);
2222 }
2223 } else {
2224 print $stdout
2225 "-- sorry, couldn't get a supported WOEID for your location.\n";
2226 }
2227 return 0;
2228 }
2229
2230 1 if (s/^\/#([^\s]+)/\/tron #\1/);
2231 # /# command falls through to tron
2232 if (s/^\/tron\s+// && s/\s*$// && length) {
2233 $_ = lc($_);
2234 $track .= " " if (length($track));
2235 $_ = "/set track ${track}$_";
2236 # fall through to set
2237 }
2238 if (/^\/track ([^ ]+)/) {
2239 s#^/#/set #;
2240 # and fall through to set
2241 }
2242
2243 # /listoff
2244 if (s/^\/list?off\s+// && s/\s*$// && length) {
2245 if (/,/ || /\s+/) {
2246 print $stdout "-- one list at a time please\n";
2247 return 0;
2248 }
2249 if (!scalar(@listlist)) {
2250 print $stdout
2251 "-- ok! that was easy! (you don't have any lists in your timeline)\n";
2252 return 0;
2253 }
2254 my $w;
2255 my $newlists = '';
2256 my $didfilter = 0;
2257 foreach $w (@listlist) {
2258 my $x = join('/', @{ $w });
2259 if ($x eq $_ || "$whoami$_" eq $x ||
2260 "$whoami/$_" eq $x) {
2261 print $stdout "*** ok, filtered $x\n";
2262 $didfilter = 1;
2263 } else {
2264 $newlists .= (length($newlists)) ? ",$x"
2265 : $x;
2266 }
2267 }
2268 if ($didfilter) {
2269 &setvariable('lists', $newlists, 1);
2270 } else {
2271 print $stdout "*** hmm, no such list? current value:\n";
2272 print $stdout "*** lists => ",
2273 &getvariable('lists'), "\n";
2274 }
2275 return 0;
2276 }
2277
2278 # /liston
2279 if (s/^\/list?on\s+// && s/\s*$// && length) {
2280 if (/,/ || /\s+/) {
2281 print $stdout "-- one list at a time please\n";
2282 return 0;
2283 }
2284 my $uname;
2285 my $lname;
2286 if (m#/#) {
2287 ($uname, $lname) = split(m#/#, $_, 2);
2288 } else {
2289 $lname = $_;
2290 $uname = '';
2291 }
2292 if (!length($uname) && $anonymous) {
2293 print $stdout
2294 "-- you must specify a username for a list when anonymous.\n";
2295 return 0;
2296 }
2297 $uname ||= $whoami;
2298
2299 # check the list validity
2300 my $my_json_ref = &grabjson(
2301 "${statusliurl}?owner_screen_name=${uname}&slug=${lname}",
2302 0, 0, 0, undef, 1);
2303 if (!$my_json_ref || ref($my_json_ref) ne 'ARRAY') {
2304 print $stdout
2305 "*** list $uname/$lname seems bogus; not added\n";
2306 return 0;
2307 }
2308
2309 $_ = "/add lists $uname/$lname";
2310 # fall through to add
2311 }
2312 if (s/^\/a(uto)?lists?\s+// && s/\s*$// && length) {
2313 s/\s+/,/g if (!/,/);
2314 print $stdout
2315 "--- warning: lists aren't checked en masse; make sure they exist\n";
2316 $_ = "/set lists $_";
2317 # and fall through to set
2318 }
2319
2320 # setter for internal value settings
2321 # shortcut for boolean settings
2322 if (/^\/s(et)? ([^ ]+)\s*$/) {
2323 my $key = $2;
2324 $_ = "/set $key 1"
2325 if($opts_boolean{$key} && $opts_can_set{$key});
2326 # fall through to three argument version
2327 }
2328 if (/^\/uns(et)? ([^ ]+)\s*$/) {
2329 my $key = $2;
2330 if ($opts_can_set{$key} && $opts_boolean{$key}) {
2331 &setvariable($key, 0, 1);
2332 return 0;
2333 }
2334 &setvariable($key, undef, 1);
2335 return 0;
2336 }
2337 # stubs out to set variable
2338 if (/^\/s(et)? ([^ ]+) (.+)\s*$/) {
2339 my $key = $2;
2340 my $value = $3;
2341 &setvariable($key, $value, 1);
2342 return 0;
2343 }
2344 # append to a variable (if not boolean)
2345 if (/^\/ad(d)? ([^ ]+) (.+)\s*$/) {
2346 my $key = $2;
2347 my $value = $3;
2348 if ($opts_boolean{$key}) {
2349 print $stdout
2350 "*** why are you appending to a boolean?\n";
2351 return 0;
2352 }
2353 if (length(&getvariable($key))) {
2354 $value = " $value" if ($opts_space_delimit{$key});
2355 $value = ",$value" if ($opts_comma_delimit{$key});
2356 }
2357 &setvariable($key, &getvariable($key).$value, 1);
2358 return 0;
2359 }
2360 # delete from a variable (if not boolean)
2361 if (/^\/del ([^ ]+) (.+)\s*$/) {
2362 my $key = $1;
2363 my $value = $2;
2364 my $old;
2365 if ($opts_boolean{$key}) {
2366 print $stdout
2367 "*** why are you deleting from a boolean?\n";
2368 return 0;
2369 }
2370 if (!length($old = &getvariable($key))) {
2371 print $stdout "*** $key is already empty\n";
2372 return 0;
2373 }
2374 my $del =
2375 ($opts_space_delimit{$key}) ? '\s+' :
2376 ($opts_comma_delimit{$key}) ? '\s*,\s*' :
2377 undef;
2378 if (!defined($del)) {
2379 # simple substitution
2380 1 while ($old =~ s/$value//g);
2381 } else {
2382 1 while ($old =~ s/$del$value($del)/\1/g);
2383 1 while ($old =~ s/^$value$del//);
2384 1 while ($old =~ s/$del$value//);
2385 }
2386 &setvariable($key, $old, 1);
2387 return 0;
2388 }
2389 # I thought about implementing a /pdel but besides being ugly
2390 # I don't think most people will push a truncated setting. tell me
2391 # if I'm wrong.
2392
2393 # stackable settings
2394 if (/^\/pu(sh)? ([^ ]+)\s*$/) {
2395 my $key = $2;
2396 if ($opts_can_set{$key}) {
2397 if ($opts_boolean{$key}) {
2398 $_ = "/push $key 1";
2399 # fall through to three argument version
2400 } else {
2401 if (!$opts_can_set{$key}) {
2402 print $stdout
2403 "*** setting is not stackable: $key\n";
2404 return 0;
2405 }
2406 my $old = &getvariable($key);
2407 push(@{ $push_stack{$key} }, $old);
2408 print $stdout
2409 "--- saved on stack for $key: $old\n";
2410 return 0;
2411 }
2412 }
2413 }
2414
2415 # common code for set and append
2416 if (/^\/(pu|push|pad|padd) ([^ ]+) (.+)\s*$/) {
2417 my $comm = $1;
2418 my $key = $2;
2419 my $value = $3;
2420 $comm = ($comm =~ /^pu/) ? "push" : "padd";
2421 if ($opts_boolean{$key} && $comm eq 'padd') {
2422 print $stdout
2423 "*** why are you appending to a boolean?\n";
2424 return 0;
2425 }
2426 if (!$opts_can_set{$key}) {
2427 print $stdout
2428 "*** setting is not stackable: $key\n";
2429 return 0;
2430 }
2431 my $old = &getvariable($key);
2432 $old += 0 if ($opts_boolean{$key});
2433 push(@{ $push_stack{$key} }, $old);
2434 print $stdout "--- saved on stack for $key: $old\n";
2435 if ($comm eq 'padd' && length($old)) {
2436 $value = " $value" if ($opts_space_delimit{$key});
2437 $value = ",$value" if ($opts_comma_delimit{$key});
2438 $old .= $value;
2439 } else {
2440 $old = $value;
2441 }
2442 &setvariable($key, $old, 1);
2443 return 0;
2444 }
2445 # we assume that if the setting is in the push stack, it's valid
2446 if (/^\/pop ([^ ]+)\s*$/) {
2447 my $key = $1;
2448 if (!scalar(@{ $push_stack{$key} })) {
2449 print $stdout
2450 "*** setting is not stacked: $key\n";
2451 return 0;
2452 }
2453 &setvariable($key, pop(@{ $push_stack{$key} }), 1);
2454 return 0;
2455 }
2456
2457 # shell escape
2458 if (s/^\/\!// && s/\s*$// && length) {
2459 system("$_");
2460 $x = $? >> 8;
2461 print $stdout "*** exited with $x\n" if ($x);
2462 return 0;
2463 }
2464
2465 if ($_ eq '/help' || $_ eq '/?') {
2466 print <<'EOF';
2467 [1 of 5]
2468 *** BASIC COMMANDS: :a$AAOOOOOOOOOOOOOOOOOAA$a, ==================
2469 +@A:. .:B@+ ANYTHING WITHOUT
2470 /refresh =@B HELP!!! HELP!!! B@= A LEADING / IS
2471 grabs the newest :a$Ao oA$a, SENT AS A TWEET!
2472 tweets right ;AAA$a; :a$AAAAAAAAAAA; ==================
2473 away (or tells :AOaaao:, .:oA*:. JUST TYPE TO TALK!
2474 you if there .;=$$$OBO***+ .+aaaa$:
2475 is nothing new) :*; :***O@Aaaa*o, ============
2476 by thumping .+++++: o#o REMEMBER!!
2477 the background :OOOOOOA*:::, =@o ,:::::. ============
2478 process. .+++++++++: =@*.....=a$OOOB#; MANY COMMANDS, AND
2479 =@OoO@BAAA#@$o, ALL TWEETS ARE
2480 /again =@o .+aaaaa: --ASYNCHRONOUS--
2481 displays most recent =@Aaaaaaaaaa*o*a;, and might not always
2482 tweets, both old and =@$++=++++++:,;+aA: respond
2483 new. ,+$@*.=O+ ...oO; oAo+. immediately!
2484 ,+o$OO=.+aA#####Oa;.*OO$o+.
2485 /dm and /dmagain for DMs. +Ba::;oaa*$Aa=aA$*aa=;::$B:
2486 ,===O@BOOOOOOOOO#@$===,
2487 /replies o@BOOOOOOOOO#@+ ==================
2488 shows replies and mentions. o@BOB@B$B@BO#@+ USE + FOR A COUNT:
2489 o@*.a@o a@o.$@+ /re +30 => last 30 replies
2490 /quit resumes your boring life. o@B$B@o a@A$#@+ ==========================
2491
2492 EOF
2493 &linein("PRESS RETURN/ENTER>");
2494 print <<"EOF";
2495 [2 of 5]
2496 +- MORE COMMANDS --+ -=-=- USER STUFF -=-=-
2497 | | /whois username displays info about username
2498 | See the oysttyer | /again username views their most recent tweets
2499 | home page for | /wagain username combines them all
2500 | complete list | /follow username follow a username
2501 | | /leave username stop following a username
2502 +----------------- +
2503
2504 EOF
2505 &linein("PRESS RETURN/ENTER>");
2506 print <<"EOF";
2507 [3 of 5]
2508 +--- TWEET SELECTION --------------------------------------------------------+
2509 | all tweets have menu codes (letters + number). example: |
2510 | a5> <oysttyer> Send me Dr Pepper https://oysttyer.github.io/ |
2511 | /reply a5 message replies to tweet a5 |
2512 | example: /reply a5 I also like Dr Pepper |
2513 | becomes \@oysttyer I also like Dr Pepper (and is threaded) |
2514 | /thread a5 if a5 is part of a thread (the username |
2515 | has a \@ or \") then show all posts up |
2516 | to that |
2517 | /url a5 opens all URLs in tweet a5 |
2518 | Mac OS X users, do first: /set urlopen open %U |
2519 | Dummy terminal users, try /set urlopen lynx -dump %U | more |
2520 | /delete a5 deletes tweet a5, if it's your tweet |
2521 | /rt a5 <optional message> retweets (or quotes) tweet a5 |
2522 | example: /rt a5 |
2523 | becomes: RT \@oysttyer: Send me... |
2524 | example: /rt a5 message |
2525 | becomes: Some smart comment about [tweet a5] |
2526 +--- Abbreviations: /re, /th, /url, /del --- menu codes wrap around at end --+
2527
2528 EOF
2529 &linein("PRESS RETURN/ENTER>");
2530 print <<"EOF";
2531 [4 of 5]
2532 +--- DM SPECIFIC ------------------------------------------------------------+
2533 | all DMs have menu codes (letters + number, prefixed with d). example: |
2534 | [DM da0][oysttyer/Sun Jan 32 1969] I think you are cute |
2535 | /dm username message send a username a DM |
2536 | /qdm a5 username <optional text> Share a tweet via a DM |
2537 | example: /qdm a5 \@oysttyer A secret comment about this tweet |
2538 | becomes: d oysttyer A secret comment about this tweet https://... |
2539 | /edm username message Opens message in \$EDITOR before sending |
2540 | /edmreply da0 message Also opens message in \$EDITOR |
2541 +---------------------------------------------------------------------------+
2542 =====> /reply, /delete and /url work for direct message menu codes too! <=====
2543
2544 EOF
2545 &linein("PRESS RETURN/ENTER>");
2546 print <<"EOF";
2547 [5 of 5]
2548 Use /set to turn on options or set them at runtime. There is a BIG LIST!
2549
2550 >> EXAMPLE: WANT ANSI? /set ansi 1
2551 or use the -ansi command line option.
2552 WANT TO VERIFY YOUR TWEETS BEFORE POSTING? /set verify 1
2553 or use the -verify command line option.
2554 For more, like readline support, UTF-8, SSL, proxies, etc., see the docs.
2555
2556 ** READ THE COMPLETE DOCUMENTATION: https://oysttyer.github.io/
2557
2558 oysttyer $oysttyer_VERSION is (c)2017 oysttyer organisation
2559 (c)2007-2012 cameron kaiser + contributors.
2560 all rights reserved. this software is offered AS IS, with no guarantees. it
2561 is not endorsed by Obvious or the executives and developers of Twitter.
2562
2563 *** subscribe to updates at http://twitter.com/oysttyer
2564 submit your suggestions at https://github.com/oysttyer/oysttyer
2565
2566 EOF
2567 return 0;
2568 }
2569 if ($_ eq '/ruler' || $_ eq '/ru') {
2570 my ($prompt, $prolen) = (&$prompt(1));
2571 $prolen = " " x $prolen;
2572 print $stdout <<"EOF";
2573 ${prolen} 1 2 3 4 5 6 7 8 9 0 1 2 3 XX
2574 ${prompt}1...5....0....5....0....5....0....5....0....5....0....5....0....5....0....5....0....5....0....5....0....5....0....5....0....5....0....5...XX
2575 EOF
2576 return 0;
2577 }
2578 if ($_ eq '/cls' || $_ eq '/clear') {
2579 if ($ansi) {
2580 print $stdout "${ESC}[H${ESC}[2J\n";
2581 } else {
2582 print $stdout ("\n" x ($ENV{'ROWS'} || 50));
2583 }
2584 return 0;
2585 }
2586 if ($_ eq '/refresh' || $_ eq '/thump' || $_ eq '/r') {
2587 print $stdout "-- /refresh in streaming mode is pretty impatient\n"
2588 if ($dostream);
2589 &thump;
2590 return 0;
2591 }
2592 if (m#^/a(gain)?(\s+\+\d+)?$#) { # the asynchronous form
2593 my $countmaybe = $2;
2594 $countmaybe =~ s/[^\d]//g if (length($countmaybe));
2595 $countmaybe += 0;
2596 if ($countmaybe > 999) {
2597 print $stdout "-- greedy bastard, try +fewer.\n";
2598 return 0;
2599 }
2600 $countmaybe = sprintf("%03i", $countmaybe);
2601 print $stdout "-- background request sent\n" unless ($synch);
2602
2603 print C "reset${countmaybe}-----------\n";
2604 &sync_semaphore;
2605 return 0;
2606 }
2607
2608 # this is for users -- list form is below
2609 if ($_ =~ m#^/(w)?a(gain)?\s+(\+\d+\s+)?([^\s/]+)$#) { #synchronous form
2610 my $mode = $1;
2611 my $uname = lc($4);
2612
2613 my $countmaybe = $3;
2614 $countmaybe =~ s/[^\d]//g if (length($countmaybe));
2615 $countmaybe += 0;
2616
2617 $uname =~ s/^\@//;
2618 $readline_completion{'@'.$uname}++ if ($termrl);
2619 print $stdout
2620 "-- synchronous /again command for $uname ($countmaybe)\n"
2621 if ($verbose);
2622 my $my_json_ref =
2623 &grabjson("${uurl}?screen_name=${uname}&include_rts=true",
2624 0, 0, $countmaybe, undef, 1);
2625 &dt_tdisplay($my_json_ref, 'again');
2626 unless ($mode eq 'w' || $mode eq 'wf') {
2627 return 0;
2628 } # else fallthrough
2629 }
2630 if ($_ =~ m#^/w(hois|a|again)?\s+(\+\d+\s+)?\@?([^\s]+)#) {
2631 my $uname = lc($3);
2632 $uname =~ s/^\@//;
2633 $readline_completion{'@'.$uname}++ if ($termrl);
2634 print $stdout "-- synchronous /whois command for $uname\n"
2635 if ($verbose);
2636 my $my_json_ref =
2637 &grabjson("${wurl}?screen_name=${uname}", 0, 0, 0, undef, 1);
2638
2639 if (defined($my_json_ref) && ref($my_json_ref) eq 'HASH' &&
2640 length($my_json_ref->{'screen_name'})) {
2641 my $sturl = undef;
2642 my $purl =
2643 &descape($my_json_ref->{'profile_image_url'});
2644 if ($avatar && length($purl) && $purl !~
2645 m#^http://[^.]+\.(twimg\.com|twitter\.com).+/images/default_profile_\d+_normal.png#) {
2646 my $exec = $avatar;
2647 my $fext;
2648 ($purl =~ /\.([a-z0-9A-Z]+)$/) &&
2649 ($fext = $1);
2650 if ($purl !~ /['\\]/) { # careful!
2651 $exec =~ s/\%U/'$purl'/g;
2652 $exec =~ s/\%N/$uname/g;
2653 $exec =~ s/\%E/$fext/g;
2654 print $stdout "\n";
2655 print $stdout "($exec)\n"
2656 if ($verbose);
2657 system($exec);
2658 }
2659 }
2660 print $streamout "\n";
2661 &userline($my_json_ref, $streamout);
2662 print $streamout &wwrap(
2663 "\"@{[ &strim(&descape($my_json_ref->{'description'})) ]}\"\n")
2664 if (length(&strim($my_json_ref->{'description'})));
2665 if (length($my_json_ref->{'url'})) {
2666 $sturl =
2667 $urlshort = &descape($my_json_ref->{'url'});
2668 $urlshort =~ s/^\s+//;
2669 $urlshort =~ s/\s+$//;
2670 print $streamout "${EM}URL:${OFF}\t\t$urlshort\n";
2671 }
2672 print $streamout &wwrap(
2673 "${EM}Location:${OFF}\t@{[ &descape($my_json_ref->{'location'}) ]}\n")
2674 if (length($my_json_ref->{'location'}));
2675 print $streamout <<"EOF";
2676 ${EM}Picture:${OFF}\t@{[ &descape($my_json_ref->{'profile_image_url'}) ]}
2677
2678 EOF
2679 unless ($anonymous || $whoami eq $uname) {
2680 my $g = &grabjson(
2681 "$frurl?source_screen_name=$whoami&target_screen_name=$uname", 0, 0, 0,
2682 undef, 1);
2683 print $streamout &wwrap(
2684 "${EM}Do you follow${OFF} this user? ... ${EM}$g->{'relationship'}->{'target'}->{'followed_by'}${OFF}\n")
2685 if (ref($g) eq 'HASH');
2686 my $g = &grabjson(
2687 "$frurl?source_screen_name=$uname&target_screen_name=$whoami", 0, 0, 0,
2688 undef, 1);
2689 print $streamout &wwrap(
2690 "${EM}Does this user follow${OFF} you? ... ${EM}$g->{'relationship'}->{'target'}->{'followed_by'}${OFF}\n")
2691 if (ref($g) eq 'HASH');
2692 print $streamout "\n";
2693 }
2694 print $stdout &wwrap(
2695 "-- %URL% is now $urlshort (/short shortens, /url opens)\n")
2696 if (defined($sturl));
2697 }
2698 return 0;
2699 }
2700
2701 if (m#^/(df|doesfollow)\s+\@?([^\s]+)$#) {
2702 if ($anonymous) {
2703 print $stdout "-- who follows anonymous anyway?\n";
2704 return 0;
2705 }
2706 $_ = "/doesfollow $2 $whoami";
2707 print $stdout "*** assuming you meant: $_\n";
2708 # fall through to ...
2709 }
2710 if (m#^/(df|doesfollow)\s+\@?([^\s]+)\s+\@?([^\s]+)$#) {
2711 my $user_a = $2;
2712 my $user_b = $3;
2713 if ($user_a =~ m#/# || $user_b =~ m#/#) {
2714 print $stdout "--- sorry, this won't work on lists.\n";
2715 return 0;
2716 }
2717 my $g = &grabjson(
2718 "${frurl}?source_screen_name=${user_a}&target_screen_name=${user_b}", 0, 0, 0,
2719 undef, 1);
2720 if ($msg = &is_json_error($g)) {
2721 print $stdout <<"EOF";
2722 ${MAGENTA}*** warning: server error message received
2723 *** "$ec"${OFF}
2724 EOF
2725 } elsif ($g->{'relationship'}->{'target'}) {
2726 print $stdout "--- does $user_a follow ${user_b}? => ";
2727 print $streamout "$g->{'relationship'}->{'target'}->{'followed_by'}\n"
2728 } else {
2729 print $stdout
2730 "-- sorry, bogus server response, try again later.\n";
2731 }
2732 return 0;
2733 }
2734
2735 # this is dual-headed and supports both lists and regular followers.
2736 if(s#^/(frs|friends|fos|followers)(\s+\+\d+)?\s*##) {
2737 my $countmaybe = $2;
2738 my $mode = $1;
2739 my $arg = lc($_);
2740 my $lname = '';
2741 my $user = '';
2742 my $what = '';
2743 $arg =~ s/^@//;
2744 $who = $arg;
2745 ($who, $lname) = split(m#/#, $arg, 2) if (m#/#);
2746 if (length($lname) && !length($user) && $anonymous) {
2747 print $stdout
2748 "-- you must specify a username for a list when anonymous.\n";
2749 return 0;
2750 }
2751 $who ||= $whoami;
2752 if (!length($lname)) {
2753 $what = ($mode eq 'frs' || $mode eq 'friends')
2754 ? "friends" : "followers";
2755 $mode = ($mode eq 'frs' || $mode eq 'friends')
2756 ? $friendsurl : $followersurl;
2757 } else {
2758 $what = ($mode eq 'frs' || $mode eq 'friends')
2759 ? "friends/members" : "followers/subscribers";
2760 $mode = ($mode eq 'frs' || $mode eq 'friends')
2761 ? $getliurl : $getfliurl;
2762 $user = "&owner_screen_name=${who}&slug=${lname}";
2763 $who = "list $who/$lname";
2764 }
2765 $countmaybe =~ s/[^\d]//g if (length($countmaybe));
2766 $countmaybe += 0;
2767 $countmaybe ||= 20;
2768
2769 # we use the undocumented count= support to, by default,
2770 # reduce the JSON parsing overhead. if we always had to take
2771 # all 100, we really eat it on parsing. the downside is that,
2772 # per @episod, the stuff we get is "less" fresh.
2773 my $countper = ($countmaybe < 100) ? $countmaybe : 100;
2774
2775 if (!length($lname)) {
2776 # we need to get IDs, then call lookup. right now it's
2777 # limited to 5000 because that is the limit for API 1.1
2778 # without having to do pagination here too. sorry.
2779 if ($countmaybe >= 5000) {
2780 print $stdout
2781 "-- who do you think you are? Scoble? currently limited to 4999 or less\n";
2782 return 0;
2783 }
2784
2785 # grab all the IDs
2786 my $ids_ref = &grabjson(
2787 "$mode?count=${countmaybe}&screen_name=${who}&stringify_ids=true",
2788 0, 0, 0, undef, 1);
2789 return 0 if (!$ids_ref || ref($ids_ref) ne 'HASH' ||
2790 !$ids_ref->{'ids'});
2791 $ids_ref = $ids_ref->{'ids'};
2792 return 0 if (ref($ids_ref) ne 'ARRAY');
2793 my @ids = @{ $ids_ref };
2794 @ids = sort { 0+$a <=> 0+$b } @ids;
2795 # make it somewhat deterministic
2796
2797 my $dount = &min($countmaybe, scalar(@ids));
2798 my $swallow = &min(100, $dount);
2799 my @usarray = undef; shift(@usarray); # force underflow
2800 my $l_ref = undef;
2801
2802 # for each block of $countper, emit
2803 my $printed = 0;
2804
2805 FFABIO: while ($dount--) {
2806 if (!scalar(@usarray)) {
2807 my @next_ids;
2808
2809 last FFABIO if (!scalar(@ids));
2810
2811 # if we asked for less than 100, get
2812 # that. otherwise,
2813 # get the top 100 off that list (or
2814 # the list itself, if 100 or less)
2815 if (scalar(@ids) <= $swallow) {
2816 @next_ids = @ids;
2817 @ids = ();
2818 } else {
2819 @next_ids =
2820 @ids[0..($swallow-1)];
2821 @ids = @ids[$swallow..$#ids];
2822 }
2823
2824 # turn it into a list to pass to
2825 # lookupidurl and get the list
2826 $l_ref = &postjson($lookupidurl,
2827 "user_id=".&url_oauth_sub(join(',', @next_ids)));
2828 last FFABIO if(ref($l_ref) ne 'ARRAY');
2829 @usarray = sort
2830 { 0+($a->{'id'}) <=> 0+($b->{'id'}) }
2831 @{ $l_ref };
2832 last if (!scalar(@usarray));
2833 }
2834 &$userhandle(shift(@usarray));
2835 $printed++;
2836 }
2837 print $stdout "-- sorry, no $what found for $who.\n"
2838 if (!$printed);
2839 return 0;
2840 }
2841
2842 # lists
2843 # loop through using the cursor until desired number.
2844 my $cursor = -1; # initial value
2845 my $printed = 0;
2846 my $nofetch = 0;
2847 my $json_ref = undef;
2848 my @usarray = undef; shift(@usarray); # force underflow
2849
2850 # this is a simpler version of the above.
2851 FABIO: while($countmaybe--) {
2852 if(!scalar(@usarray)) {
2853 last FABIO if ($nofetch);
2854 $json_ref = &grabjson(
2855 "${mode}?count=${countper}&cursor=${cursor}${user}",
2856 0, 0, 0, undef, 1);
2857 @usarray = @{ $json_ref->{'users'} };
2858 last FABIO if (!scalar(@usarray));
2859 $cursor = $json_ref->{'next_cursor_str'} ||
2860 $json_ref->{'next_cursor'} || -1;
2861 $nofetch = ($cursor < 1) ? 1 : 0;
2862 }
2863 &$userhandle(shift(@usarray));
2864 $printed++;
2865 }
2866 print $stdout "-- sorry, no $what found for $who.\n"
2867 if (!$printed);
2868 return 0;
2869 }
2870
2871 # threading
2872 if (m#^/th(read)?\s+(\+\d+\s+)?([zZ]?[a-zA-Z]?[0-9]+)$#) {
2873 my $countmaybe = $2;
2874 if (length($countmaybe)) {
2875 print $stdout
2876 "-- /thread does not (yet) support +count\n";
2877 return 0;
2878 }
2879 my $code = lc($3);
2880 my $tweet = &get_tweet($code);
2881 if (!defined($tweet)) {
2882 print $stdout "-- no such tweet (yet?): $code\n";
2883 return 0;
2884 }
2885 my $limit = 9;
2886 my $id = $tweet->{'retweeted_status'}->{'id_str'} ||
2887 $tweet->{'in_reply_to_status_id_str'} ||
2888 $tweet->{'quoted_status_id_str'};
2889 my $thread_ref = [ $tweet ];
2890 while ($id && $limit) {
2891 print $stdout "-- thread: fetching $id\n"
2892 if ($verbose);
2893 my $next = &grabjson("${idurl}?id=${id}", 0, 0, 0,
2894 undef, 1);
2895 $id = 0;
2896 $limit--;
2897 if (defined($next) && ref($next) eq 'HASH') {
2898 push(@{ $thread_ref },
2899 &fix_geo_api_data($next));
2900 $id = $next->{'retweeted_status'}->{'id_str'}
2901 || $next->{'in_reply_to_status_id_str'}
2902 || $next->{'quoted_status_id_str'}
2903 || 0;
2904 }
2905 }
2906 &tdisplay($thread_ref, 'thread', 0, 1); # use the mini-menu
2907 return 0;
2908 }
2909
2910 # pull out entities. this works for DMs and tweets.
2911 # btw: T.CO IS WACK.
2912 if (m#^/ent?(ities)? ([dDzZ]?[a-zA-Z]?[0-9]+)$#) {
2913 my $v;
2914 my $w;
2915 my $thing;
2916 my $genurl;
2917 my $code = lc($2);
2918 my $hash;
2919 if ($code !~ /[a-z]/) {
2920 # this is an optimization: we don't need to get
2921 # the old tweet since we're going to fetch it anyway.
2922 $hash = { "id_str" => $code };
2923 $thing = "tweet";
2924 $genurl = $idurl;
2925 } elsif ($code =~ /^d.[0-9]+$/) {
2926 $hash = &get_dm($code);
2927 $thing = "DM";
2928 $genurl = $dmidurl;
2929 } else {
2930 $hash = &get_tweet($code);
2931 $thing = "tweet";
2932 $genurl = $idurl;
2933 }
2934
2935 if (!defined($hash)) {
2936 print $stdout "-- no such $thing (yet?): $code\n";
2937 return 0;
2938 }
2939
2940 my $id = $hash->{'id_str'};
2941 $hash = &grabjson("${genurl}?id=${id}", 0, 0, 0, undef, 1);
2942 if (!defined($hash) || ref($hash) ne 'HASH') {
2943 print $stdout "-- failed to get entities from server, sorry\n";
2944 return 0;
2945 }
2946
2947 # if a retweeted status, get the status.
2948 $hash = $hash->{'retweeted_status'}
2949 if (defined($hash->{'retweeted_status'}) &&
2950 ref($hash->{'retweeted_status'}) eq 'HASH');
2951
2952 my $didprint = 0;
2953 my $entitiesprint = 0;
2954 # Twitter puts entities in multiple fields.
2955 # Target extended_entities, originally based on following from @myshkin (github) / @justarobert (twitter)
2956 # from: https://gist.github.com/myshkin/5bfb2f5e795bc2cf2146#file-gistfile1-pl
2957 foreach my $entities (qw(entities extended_entities)) {
2958 $entitiesprint = 1;
2959 foreach $type (qw(media urls)) {
2960 my $array = $hash->{$entities}->{$type};
2961 next if (!defined($array) || ref($array) ne 'ARRAY');
2962 foreach $entry (@{ $array }) {
2963 next if (!defined($entry) || ref($entry) ne 'HASH');
2964 next if (!length($entry->{'url'}) ||
2965 (!length($entry->{'expanded_url'}) &&
2966 !length($entry->{'media_url'})));
2967 if ($entitiesprint) {
2968 print $stdout "$entities:\n";
2969 $entitiesprint = 0;
2970 }
2971 my $u1 = &descape($entry->{'url'});
2972 if (defined($entry->{'video_info'})) {
2973 foreach $variant (@{ $entry->{'video_info'}->{'variants'} }) {
2974 my $videourl = &descape($variant->{'url'});
2975 print $stdout "$u1 => $videourl\n";
2976 }
2977 }
2978 else {
2979 my $u2 = &descape($entry->{'expanded_url'});
2980 my $u3 = &descape($entry->{'media_url'});
2981 my $u4 = &descape($entry->{'media_url_https'});
2982 $u2 = $u4 || $u3 || $u2;
2983 print $stdout "$u1 => $u2\n";
2984 }
2985 #To stay compliant with TOS we can only open the tco.
2986 $urlshort = $u1;
2987 $didprint++;
2988 }
2989 }
2990 }
2991 if ($didprint) {
2992 print $stdout &wwrap(
2993 "-- %URL% is now $urlshort (/url opens)\n");
2994 } else {
2995 print $stdout "-- no entities or URLs found\n";
2996 }
2997 return 0;
2998 }
2999
3000 if (($_ eq '/url' || $_ eq '/open') && length($urlshort)) {
3001 $_ = "/url $urlshort";
3002 print $stdout "*** assuming you meant %URL%: $_\n";
3003 # and fall through to ...
3004 }
3005 if (m#^/(url|open)\s+(http|gopher|https|ftp)://.+# &&
3006 s#^/(url|open)\s+##) {
3007 &openurl($_);
3008 return 0;
3009 }
3010 if (m#^/(url|open|web) ([dDzZ]?[a-zA-Z]?[0-9]+)$#) {
3011 my $code = lc($2);
3012 my $tweet;
3013 my $genurl = undef;
3014 $urlshort = undef;
3015
3016 if ($code =~ /^d/ && length($code) > 2) {
3017 $tweet = &get_dm($code); # USO!
3018 if (!defined($tweet)) {
3019 print $stdout
3020 "-- no such DM (yet?): $code\n";
3021 return 0;
3022 }
3023 $genurl = $dmidurl;
3024 } else {
3025 $tweet = &get_tweet($code);
3026 if (!defined($tweet)) {
3027 print $stdout
3028 "-- no such tweet (yet?): $code\n";
3029 return 0;
3030 }
3031 $genurl = $idurl;
3032 }
3033
3034 # Just open the link to the tweet itself
3035 if (m#^/web#) {
3036 # DMs don't have links
3037 if ($code =~ /^d[${alphabet}]/) {
3038 print "*** DMs don\'t have links\n";
3039 } else {
3040 &openurl("${http_proto}://twitter.com/$tweet->{'user'}->{'screen_name'}/statuses/$tweet->{'id_str'}");
3041 }
3042 return 0;
3043 }
3044 # to be TOS-compliant, we must try entities first to use
3045 # t.co wrapped links. this is a tiny version of /entities.
3046 unless ($notco) {
3047 my $id = $tweet->{'retweeted_status'}->{'id_str'}
3048 || $tweet->{'id_str'};
3049 my $hash;
3050
3051 # only fetch if we have to. if we already fetched
3052 # because we were given a direct id_str instead of a
3053 # menu code, then we already have the entities.
3054 if ($code !~ /^[0-9]+$/) {
3055 $hash = &grabjson("${genurl}?id=${id}",
3056 0, 0, 0, undef, 1);
3057 } else {
3058 # MAKE MONEY FAST WITH OUR QUICK CACHE PLAN
3059 $hash = $tweet;
3060 }
3061 if (defined($hash) && ref($hash) eq 'HASH') {
3062 my $w;
3063 my $v;
3064 my $didprint = 0;
3065
3066 # Twitter puts entities in multiple fields. Now also target extended_entities
3067 # Unfortunately if TOS-compliance means opening t.co links then Twitter uses one link for all photos, videos, etc
3068 # so... no point opening multiple links if the same. Use hash to avoid duplicates
3069 my $links = {};
3070 foreach my $entities (qw(entities extended_entities)) {
3071 foreach $type (qw(media urls)) {
3072 my $array = $hash->{$entities}->{$type};
3073 next if (!defined($array) ||
3074 ref($array) ne 'ARRAY');
3075 foreach $entry (@{ $array }) {
3076 next if (!defined($entry) ||
3077 ref($entry) ne 'HASH');
3078 next if (!length($entry->{'url'}) ||
3079 (!length($entry->{'expanded_url'}) &&
3080 !length($entry->{'media_url'})));
3081 my $u1 = &descape($entry->{'url'});
3082 $links->{$u1} = 1;
3083 }
3084 }
3085 }
3086 while (( $link, $_l ) = each %$links ) {
3087 &openurl($link);
3088 $didprint++;
3089 }
3090 print $stdout
3091 "-- sorry, couldn't find any URL.\n"
3092 if (!$didprint);
3093 return 0;
3094 }
3095 print $stdout
3096 "-- unable to use t.co URLs, using fallback\n";
3097 }
3098 # that failed, so fall back on the old method.
3099 my $text = &descape($tweet->{'text'});
3100 # findallurls
3101 while ($text
3102 =~ s#(h?ttp|h?ttps|ftp|gopher)://([a-zA-Z0-9_~/:%\-\+\.\=\&\?\#,]+)##){
3103 # sigh. I HATE YOU TINYARRO.WS
3104 #TODO
3105 # eventually we will have to put a punycode implementation into openurl
3106 # to handle things like Mac OS X's open which don't understand UTF-8 URLs.
3107 # when we do, uncomment this again
3108 # =~ s#(http|https|ftp|gopher)://([^'\\]+?)('|\\|\s|$)##) {
3109 my $url = $1 . "://$2";
3110 $url = "h$url" if ($url =~ /^ttps?:/);
3111 $url =~ s/[\.\?]$//;
3112 &openurl($url);
3113 }
3114 print $stdout "-- sorry, couldn't find any URL.\n"
3115 if (!defined($urlshort));
3116 return 0;
3117 }
3118
3119 #TODO
3120 if (s/^\/(likes)(\s+\+\d+)?\s*//) {
3121 my $my_json_ref;
3122 my $countmaybe = $2;
3123 $countmaybe =~ s/[^\d]//g if (length($countmaybe));
3124 $countmaybe += 0;
3125
3126 if (length) {
3127 $my_json_ref = &grabjson("${favsurl}?screen_name=$_",
3128 0, 0, $countmaybe, undef, 1);
3129 } else {
3130 if ($anonymous) {
3131 print $stdout
3132 "-- sorry, you can't haz likes if you're anonymous.\n";
3133 } else {
3134 print $stdout
3135 "-- synchronous /likes user command\n"
3136 if ($verbose);
3137 $my_json_ref = &grabjson($favsurl, 0, 0,
3138 $countmaybe, undef, 1);
3139 }
3140 }
3141 if (defined($my_json_ref)
3142 && ref($my_json_ref) eq 'ARRAY') {
3143 if (scalar(@{ $my_json_ref })) {
3144 my $w = "-==- likes " x 10;
3145 $w = $EM . substr($w, 0, $wrap || 79) . $OFF;
3146 print $stdout "$w\n";
3147 &tdisplay($my_json_ref, "likes");
3148 print $stdout "$w\n";
3149 } else {
3150 print $stdout
3151 "-- no likes found, boring impartiality concluded.\n";
3152 }
3153 }
3154 &$conclude;
3155 return 0;
3156 }
3157 if (
3158 m#^/(un)?l(rt|retweet|i|ike)? ([zZ]?[a-zA-Z]?[0-9]+)$#) {
3159 my $mode = $1;
3160 my $secondmode = $2;
3161 my $code = lc($3);
3162 $secondmode = ($secondmode eq 'retweet') ? 'rt' : $secondmode;
3163 if ($mode eq 'un' && $secondmode eq 'rt') {
3164 print $stdout
3165 "-- hmm. seems contradictory. no dice.\n";
3166 return 0;
3167 }
3168 my $tweet = &get_tweet($code);
3169 if (!defined($tweet)) {
3170 print $stdout "-- no such tweet (yet?): $code\n";
3171 return 0;
3172 }
3173 &cordfav($tweet->{'id_str'}, 1,
3174 (($mode eq 'un') ? $favdelurl : $favurl),
3175 &descape($tweet->{'text'}),
3176 (($mode eq 'un') ? 'removed' : 'created'));
3177 if ($secondmode eq 'rt') {
3178 $_ = "/rt $code";
3179 # and fall through
3180 } else {
3181 return 0;
3182 }
3183 }
3184
3185 # Retweet API (including quoted tweets) and manual RTs
3186 if (s#^/([oe]?)r(etweet|t) ([zZ]?[a-zA-Z]?[0-9]+)\s*##) {
3187 my $mode = $1;
3188 my $code = lc($3);
3189 my $tweet = &get_tweet($code);
3190 if (!defined($tweet)) {
3191 print $stdout "-- no such tweet (yet?): $code\n";
3192 return 0;
3193 }
3194 # use a native retweet unless we can't (or user used /ort /ert)
3195 unless ($nonewrts || length || length($mode)) {
3196 # we don't always get rs->text, so we simulate it.
3197 my $text = &descape($tweet->{'text'});
3198 $text =~ s/^RT \@[^\s]+:\s+//
3199 if ($tweet->{'retweeted_status'}->{'id_str'});
3200 print $stdout "-- status retweeted\n"
3201 unless(&updatest($text, 1, 0, undef,
3202 $tweet->{'retweeted_status'}->{'id_str'}
3203 || $tweet->{'id_str'}));
3204 return 0;
3205 }
3206 # we can't or user requested /ert /ort
3207 if (($mode eq 'o') || ($mode eq 'e') || $nonewrts ) {
3208 $retweet = "RT @" .
3209 &descape($tweet->{'user'}->{'screen_name'}) .
3210 ": " . &descape($tweet->{'text'});
3211 if ($mode eq 'e') {
3212 &add_history($retweet);
3213 print $stdout &wwrap(
3214 "-- ok, %RT% and %% are now \"$retweet\"\n");
3215 return 0;
3216 }
3217 $_ = (length) ? "$retweet $_" : $retweet;
3218 }
3219 # otherwise it is a quote tweet
3220 $sn = &descape($tweet->{'user'}->{'screen_name'});
3221 $quoted_status_url = "${http_proto}://twitter.com/$sn/statuses/$tweet->{'id_str'}";
3222 print $stdout &wwrap("(expanded to \"$_\")");
3223 print $stdout "\n";
3224 goto TWEETPRINT; # fugly! FUGLY!
3225 }
3226
3227 if (m#^/(re)?rts?of?me?(\s+\+\d+)?$# && !$nonewrts) {
3228 #TODO
3229 # when more fields are added, integrate them over the JSON_ref
3230 my $mode = $1;
3231 my $countmaybe = $2;
3232 $countmaybe =~ s/[^\d]//g if (length($countmaybe));
3233 $countmaybe += 0;
3234
3235 my $my_json_ref = &grabjson($rtsofmeurl, 0, 0, $countmaybe);
3236 &dt_tdisplay($my_json_ref, "rtsofme");
3237 if ($mode eq 're') {
3238 $_ = '/re'; # and fall through ...
3239 } else {
3240 return 0;
3241 }
3242 }
3243 if (m#^/rts?of\s+([zZ]?[a-zA-Z]?[0-9]+)$# && !$nonewrts) {
3244 my $code = lc($1);
3245 my $tweet = &get_tweet($code);
3246 my $id;
3247
3248 if (!defined($tweet)) {
3249 print $stdout "-- no such tweet (yet?): $code\n";
3250 return 0;
3251 }
3252 $id = $tweet->{'retweeted_status'}->{'id_str'} ||
3253 $tweet->{'id_str'};
3254 if (!$id) {
3255 print $stdout "-- hmmm, that tweet is major bogus.\n";
3256 return 0;
3257 }
3258 my $url = $rtsbyurl;
3259 $url =~ s/%I/$id/;
3260 my $users_ref = &grabjson("$url", 0, 0, 100, undef, 1);
3261 return if (!defined($users_ref) || ref($users_ref) ne 'ARRAY');
3262 my $k = scalar(@{ $users_ref });
3263 if (!$k) {
3264 print $stdout
3265 "-- no known retweeters, or they're private.\n";
3266 return 0;
3267 }
3268 my $j;
3269 foreach $j (@{ $users_ref }) {
3270 &$userhandle($j->{'user'});
3271 }
3272 return 0;
3273 }
3274
3275 # enable and disable NewRTs from users
3276 # we allow this even if newRTs are off from -nonewrts
3277 if (s#^/rts(on|off)\s+## && length) {
3278 &rtsonoffuser($_, 1, ($1 eq 'on'));
3279 return 0;
3280 }
3281
3282 if (m#^/del(ete)?\s+([zZ]?[a-zA-Z]?[0-9]+)$#) {
3283 my $code = lc($2);
3284 unless ($code =~ /^d[0-9][0-9]+$/) { # this is a DM.
3285 my $tweet = &get_tweet($code);
3286 if (!defined($tweet)) {
3287 print $stdout "-- no such tweet (yet?): $code\n";
3288 return 0;
3289 }
3290 if (lc(&descape($tweet->{'user'}->{'screen_name'}))
3291 ne lc($whoami)) {
3292 print $stdout
3293 "-- not allowed to delete somebody's else's tweets\n";
3294 return 0;
3295 }
3296 print $stdout &wwrap(
3297 "-- verify you want to delete: \"@{[ &descape($tweet->{'text'}) ]}\"");
3298 print $stdout "\n";
3299 $answer = lc(&linein(
3300 "-- sure you want to delete? (only y or Y is affirmative):"));
3301 if ($answer ne 'y') {
3302 print $stdout "-- ok, tweet is NOT deleted.\n";
3303 return 0;
3304 }
3305 $lastpostid = -1 if ($tweet->{'id_str'} == $lastpostid);
3306 &deletest($tweet->{'id_str'}, 1);
3307 return 0;
3308 } # dxxx falls through to ...
3309 }
3310 # DM delete version
3311 if (m#^/del(ete)? ([dD][a-zA-Z]?[0-9]+)$#) {
3312 my $code = lc($2);
3313 my $dm = &get_dm($code);
3314 if (!defined($dm)) {
3315 print $stdout "-- no such DM (yet?): $code\n";
3316 return 0;
3317 }
3318 print $stdout &wwrap(
3319 "-- verify you want to delete: " .
3320 "(from @{[ &descape($dm->{'sender'}->{'screen_name'}) ]}) ".
3321 "\"@{[ &descape($dm->{'text'}) ]}\"");
3322 print $stdout "\n";
3323 $answer = lc(&linein(
3324 "-- sure you want to delete? (only y or Y is affirmative):"));
3325 if ($answer ne 'y') {
3326 print $stdout "-- ok, DM is NOT deleted.\n";
3327 return 0;
3328 }
3329 &deletedm($dm->{'id_str'}, 1);
3330 return 0;
3331 }
3332 # /deletelast
3333 if (m#^/de?l?e?t?e?last$#) {
3334 if (!$lastpostid) {
3335 print $stdout "-- you haven't posted yet this time!\n";
3336 return 0;
3337 }
3338 if ($lastpostid == -1) {
3339 print $stdout "-- you already deleted it!\n";
3340 return 0;
3341 }
3342 print $stdout &wwrap(
3343 "-- verify you want to delete: \"$lasttwit\"");
3344 print $stdout "\n";
3345 $answer = lc(&linein(
3346 "-- sure you want to delete? (only y or Y is affirmative):"));
3347 if ($answer ne 'y') {
3348 print $stdout "-- ok, tweet is NOT deleted.\n";
3349 return 0;
3350 }
3351 &deletest($lastpostid, 1);
3352 $lastpostid = -1;
3353 return 0;
3354 }
3355
3356 if (s#^/(v)?re(ply)? ([zZ]?[a-zA-Z]?[0-9]+) ## && length) {
3357 my $mode = $1;
3358 my $code = lc($3);
3359 unless ($code =~ /^d[0-9][0-9]+/) { # this is a DM
3360 my $tweet = &get_tweet($code);
3361 if (!defined($tweet)) {
3362 print $stdout "-- no such tweet (yet?): $code\n";
3363 return 0;
3364 }
3365 my $target = &descape($tweet->{'user'}->{'screen_name'});
3366 unless (lc($target) eq lc($whoami)) { $_ = '@' . $target . " $_"; }
3367 $in_reply_to = $tweet->{'id_str'};
3368 $expected_tweet_ref = $tweet;
3369 if ($mode eq 'v') {
3370 $_ = ".$_";
3371 }
3372 $readline_completion{'@'.lc($target)}++ if ($termrl);
3373 print $stdout &wwrap("(expanded to \"$_\")");
3374 print $stdout "\n";
3375 goto TWEETPRINT; # fugly! FUGLY!
3376 } else {
3377 # this is a DM, reconstruct it
3378 $_ = "/${mode}re $code $_";
3379 # and fall through to ...
3380 }
3381 }
3382 # DM reply version
3383 if (s#^/(dm)?re(ply)? ([dD][a-zA-Z]?[0-9]+) ## && length) {
3384 my $code = lc($3);
3385 my $dm = &get_dm($code);
3386 if (!defined($dm)) {
3387 print $stdout "-- no such DM (yet?): $code\n";
3388 return 0;
3389 }
3390 # in the future, add DM in_reply_to here
3391 my $target = &descape($dm->{'sender'}->{'screen_name'});
3392 $readline_completion{'@'.lc($target)}++ if ($termrl);
3393 $_ = "/dm $target $_";
3394 print $stdout &wwrap("(expanded to \"$_\")");
3395 print $stdout "\n";
3396 # and fall through to /dm
3397 }
3398 # Share a tweet through DM
3399 if (s#^/qdm ([zZ]?[a-zA-Z]?[0-9]+) \@?([^\s]+)\s+##) {
3400 my $code = lc($1);
3401 my $tweet = &get_tweet($code);
3402 if (!defined($tweet)) {
3403 print $stdout "-- no such tweet (yet?): $code\n";
3404 return 0;
3405 }
3406 $sn = &descape($tweet->{'user'}->{'screen_name'});
3407 $quoted_status_url = "${http_proto}://twitter.com/$sn/statuses/$tweet->{'id_str'}";
3408 return &common_split_post($_ . " " . $quoted_status_url, undef, undef, $2);
3409 }
3410 if (s#^/e(dm)?re(ply)? ([dD][a-zA-Z]?[0-9]+) ## && length) {
3411 my $code = lc($3);
3412 my $dm = &get_dm($code);
3413 if (!defined($dm)) {
3414 print $stdout "-- no such DM (yet?): $code\n";
3415 return 0;
3416 }
3417 # in the future, add DM in_reply_to here
3418 my $target = &descape($dm->{'sender'}->{'screen_name'});
3419 $readline_completion{'@'.lc($target)}++ if ($termrl);
3420 $_ = "/edm $target $_";
3421 # and fall through to edm
3422 }
3423 if (s#^/edm \@?([^\s]+)\s+## && length) {
3424
3425 # Stolen from Floodgap's texapp
3426 my $string = $_;
3427 my $target = $1;
3428 print $stdout $target;
3429 my $fn = "/tmp/oysttyer-".$$.time().".txt";
3430 my $editor = $ENV{'EDITOR'} || "/usr/bin/vi";
3431 my $can_fail = 1;
3432
3433 # try to validate, if it's not too complicated
3434 if (! -x $editor) {
3435 my $binname = $editor;
3436 if ($binname !~ /\\/) {
3437 ($binname, $crap) = split(/\s+/, $binname, 2)
3438 if ($binname =~ /\s/);
3439 if (! -x $binname) {
3440 print $stdout "-- editor $binname seems invalid; set full path to EDITOR\n";
3441 return 96;
3442 }
3443 }
3444 }
3445 if(!open(K, ">$fn")) {
3446 print $stdout "-- unable to create $fn: $!\n";
3447 return 96;
3448 }
3449 print K $string if (length($string));
3450 close(K);
3451 while ($can_fail) {
3452 # hold the background during editing
3453 &ensure_held;
3454 system("$editor $fn");
3455 &ensure_not_held;
3456
3457 if(!open(K, "$fn")) {
3458 print $stdout "-- unable to read back $fn: $!\n";
3459 return 96;
3460 }
3461 $string = '';
3462 while(<K>) {
3463 $string .= $_;
3464 }
3465 close(K);
3466 $can_fail = 0;
3467
3468 # the editor has to enforce line length
3469 if (length($string) > $dm_text_character_limit) {
3470 print $stdout "-- too long: @{[ length($string) ]} characters, max $dm_text_character_limit\n";
3471 $string = '';
3472 $can_fail = 1;
3473 }
3474 if ($can_fail) {
3475 my $answer = lc(&linein(
3476 "-- edit again? (only y or Y is affirmative):"));
3477 $can_fail = 0 unless ($answer eq 'y');
3478 }
3479 }
3480 unlink($fn) || print $stdout "-- warning: couldn't remove $fn: $!\n";
3481 $string =~ s/\s+$//;
3482 chomp($string);
3483 $string =~ s/\s+$//;
3484 if (!length($string)) {
3485 print $stdout "-- editor returned nothing, not posting\n";
3486 return 97;
3487 }
3488 #Handle newlines because otherwise they get flattened
3489 $string =~ s/\n/\\n/sg;
3490 # and fall through to dm
3491 $_ = "/dm $target $string";
3492
3493 }
3494 # replyall (based on @FunnelFiasco's extension)
3495 if (s#^/(v)?r(eply)?(to)?a(ll)? ([zZ]?[a-zA-Z]?[0-9]+) ## && length) {
3496 my $mode = $1;
3497 my $code = $5;
3498
3499 # common code from /vreply
3500 my $tweet = &get_tweet($code);
3501 if (!defined($tweet)) {
3502 print $stdout "-- no such tweet (yet?): $code\n";
3503 return 0;
3504 }
3505 my $target = &descape($tweet->{'user'}->{'screen_name'});
3506 my $text = $_;
3507 if (lc($target) eq lc($whoami)) {
3508 $_ = '';
3509 } else {
3510 $_ = '@' . $target;
3511 }
3512 $in_reply_to = $tweet->{'id_str'};
3513 $expected_tweet_ref = $tweet;
3514 if ($mode eq 'v') {
3515 $_ = ".$_";
3516 }
3517
3518 # don't repeat the target or myself; track other mentions
3519 my %did_mentions = map { $_ => 1 } (lc($target));
3520 my $reply_tweet = &descape($tweet->{'text'});
3521
3522 while($reply_tweet =~ s/\@(\w+)//) {
3523 my $name = $1;
3524 my $mame = lc($name); # preserve camel case
3525 next if ($mame eq $whoami || $did_mentions{$mame}++);
3526 if ( $_ eq '.' ) {
3527 # Save a character. They're precious.
3528 $_ .= "\@$name";
3529 } else {
3530 $_ .= " \@$name";
3531 }
3532 }
3533 $_ .= " $text";
3534
3535 # add everyone in did_mentions to readline_completion
3536 grep { $readline_completion{'@'.$_}++ } (keys %did_mentions)
3537 if ($termrl);
3538
3539 # and fall through to post
3540 print $stdout &wwrap("(expanded to \"$_\")");
3541 print $stdout "\n";
3542 goto TWEETPRINT; # fugly! FUGLY!
3543 }
3544
3545 if (m#^/re(plies)?(\s+\+\d+)?$#) {
3546 my $countmaybe = $2;
3547 $countmaybe =~ s/[^\d]//g if (length($countmaybe));
3548 $countmaybe += 0;
3549
3550 if ($anonymous) {
3551 print $stdout
3552 "-- sorry, how can anyone reply to you if you're anonymous?\n";
3553 } else {
3554 # we are intentionally not keeping track of "last_re"
3555 # in this version because it is not automatically
3556 # updated and may not act as we expect.
3557 print $stdout "-- synchronous /replies command\n"
3558 if ($verbose);
3559 my $my_json_ref = &grabjson($rurl, 0, 0, $countmaybe,
3560 undef, 1);
3561 &dt_tdisplay($my_json_ref, "replies");
3562 }
3563 return 0;
3564 }
3565
3566 # DMs
3567 if ($_ eq '/dm' || $_ eq '/dmrefresh' || $_ eq '/dmr') {
3568 &dmthump;
3569 return 0;
3570 }
3571 # /dmsent, /dmagain
3572 if (m#^/dm(s|sent|a|again)(\s+\+\d+)?$#) {
3573 my $mode = $1;
3574 my $countmaybe = $2;
3575 $countmaybe =~ s/[^\d]//g if (length($countmaybe));
3576 $countmaybe += 0;
3577 if ($countmaybe > 999) {
3578 print $stdout "-- greedy bastard, try +fewer.\n";
3579 return 0;
3580 }
3581 $countmaybe = sprintf("%03i", $countmaybe);
3582 print $stdout "-- background request sent\n" unless ($synch);
3583
3584 $mode = ($mode =~ /^s/) ? 's' : 'd';
3585 print C "${mode}mreset${countmaybe}---------\n";
3586 &sync_semaphore;
3587 return 0;
3588 }
3589 if (s#^/dm \@?([^\s]+)\s+## && length) {
3590 return &common_split_post($_, undef, undef, $1);
3591 }
3592
3593 # follow and leave users
3594 if (m#^/(follow|leave|unfollow) \@?([^\s/]+)$#) {
3595 my $m = $1;
3596 my $u = lc($2);
3597 &foruuser($u, 1,
3598 (($m eq 'follow') ? $followurl : $leaveurl),
3599 (($m eq 'follow') ? 'started' : 'stopped'));
3600 return 0;
3601 }
3602
3603 # follow and leave lists. this is, frankly, pointless; it does
3604 # nothing other than to mark you. otherwise, /liston and /listoff
3605 # actually add lists to your timeline.
3606 if (m#^/(l?follow|l?leave|l?unfollow) \@?([^\s/]*)/([^\s/]+)$#) {
3607 my $m = $1;
3608 my $uname = lc($2);
3609 my $lname = lc($3);
3610
3611 if (!length($uname) || $uname eq $whoami) {
3612 print $stdout &wwrap(
3613 "** you can't mark/unmark yourself as a follower of your own lists!\n");
3614 print $stdout &wwrap(
3615 "** to add/remove your own lists from your timeline, use /liston /listoff\n");
3616 return 0;
3617 }
3618 if ($m !~ /^l/) {
3619 print $stdout &wwrap(
3620 "-- to mark/unmark you as a follower of a list, use /lfollow /lleave\n");
3621 print $stdout &wwrap(
3622 "-- to add/remove your own lists from your timeline, use /liston /listoff\n");
3623 return 0;
3624 }
3625
3626 my $r = &postjson(
3627 ($m ne 'lfollow') ? $delfliurl : $crefliurl,
3628 "owner_screen_name=$uname&slug=$lname");
3629 if ($r) {
3630 my $t = ($m eq 'lfollow') ? "" : "un";
3631 print $stdout &wwrap(
3632 "*** ok, you are now ${t}marked as a follower of $uname/${lname}.\n");
3633 my $c = ($t eq 'un') ? "off" : "on";
3634 $t = ($t eq 'un') ? "remove from" : "add to";
3635 print $stdout &wwrap(
3636 "--- to also $t your timeline, use /list${c}\n");
3637 }
3638 return 0;
3639 }
3640
3641 # block and unblock users
3642 if (m#^/(block|unblock) \@?([^\s/]+)$#) {
3643 my $m = $1;
3644 my $u = lc($2);
3645 if ($m eq 'block') {
3646 $answer = lc(&linein(
3647 "-- sure you want to block $u? (only y or Y is affirmative):"));
3648 if ($answer ne 'y') {
3649 print $stdout "-- ok, $u is NOT blocked.\n";
3650 return 0;
3651 }
3652 }
3653 &boruuser($u, 1,
3654 (($m eq 'block') ? $blockurl : $blockdelurl),
3655 (($m eq 'block') ? 'started' : 'stopped'));
3656 return 0;
3657 }
3658
3659 # mute and unmute users
3660 if (m#^/(mute|unmute) \@?([^\s/]+)$#) {
3661 my $m = $1;
3662 my $u = lc($2);
3663 if ($m eq 'mute') {
3664 $answer = lc(&linein(
3665 "-- sure you want to mute $u? (only y or Y is affirmative):"));
3666 if ($answer ne 'y') {
3667 print $stdout "-- ok, $u is NOT muted.\n";
3668 return 0;
3669 }
3670 }
3671 &muteuser($u, 1,
3672 (($m eq 'mute') ? $muteurl : $unmuteurl),
3673 (($m eq 'mute') ? 'started' : 'stopped'));
3674 return 0;
3675 }
3676
3677 # list support
3678 # /withlist (/withlis, /with, /wl)
3679 if (s#^/(withlist|withlis|withl|with|wl)\s+([^/\s]+)\s+## &&
3680 ($lname=lc($2)) && s/\s*$// && length) {
3681 my $comm = '';
3682 my $args = '';
3683 my $dont_return = 0;
3684 if ($anonymous) {
3685 print $stdout "-- no list love for anonymous\n";
3686 return 0;
3687 }
3688 if (/\s+/) {
3689 ($comm, $args) = split(/\s+/, $_, 2);
3690 } else {
3691 $comm = $_;
3692 }
3693
3694 my $return;
3695 # this is a Twitter bug -- it will not give you the
3696 # new slug in the returned hash.
3697 my $state = "modified list $lname (WAIT! then /lists to see new slug)";
3698 if ($comm eq 'create') {
3699 my $desc;
3700 ($args, $desc) = split(/\s+/, $args, 2)
3701 if ($args =~ /\s+/);
3702 if ($args ne 'public' && $args ne 'private') {
3703 print $stdout
3704 "-- must specify public or private\n";
3705 return 0;
3706 }
3707 $state = "created new list $lname (mode $args)";
3708 $desc = "description=".&url_oauth_sub($desc)."&"
3709 if (length($desc));
3710 $return = &postjson($creliurl,
3711 "${desc}mode=$args&name=$lname");
3712 } elsif ($comm eq 'private' || $comm eq 'public') {
3713 $return = &postjson($modifyliurl,
3714 "mode=$comm&owner_screen_name=${whoami}&slug=${lname}");
3715 } elsif ($comm eq 'desc' || $comm eq 'description') {
3716 if (!length($args)) {
3717 print $stdout "-- $comm needs an argument\n";
3718 return 0;
3719 }
3720 $return = &postjson($modifyliurl,
3721 "description=".&url_oauth_sub($args).
3722 "&owner_screen_name=${whoami}&slug=${lname}");
3723 } elsif ($comm eq 'name') {
3724 if (!length($args)) {
3725 print $stdout "-- $comm needs an argument\n";
3726 return 0;
3727 }
3728 $return = &postjson($modifyliurl,
3729 "name=".&url_oauth_sub($args).
3730 "&owner_screen_name=${whoami}&slug=${lname}");
3731 $state = "RENAMED list $lname (WAIT! then /lists to see new slug)";
3732 } elsif ($comm eq 'add' || $comm eq 'adduser' ||
3733 ($comm eq 'delete' && length($args))) {
3734 my $u = ($comm eq 'delete') ? $deluliurl : $adduliurl;
3735 $state = ($comm eq 'delete')
3736 ? "user(s) deleted from list $lname"
3737 : "user(s) added to list $lname";
3738 if ($args !~ /,/ || $args =~ /\s+/) {
3739 1 while ($args =~ s/\s+/,/);
3740 }
3741 if ($args =~ /\s*,\s+/ || $args =~ /\s+,\s*/) {
3742 1 while ($args =~ s/\s+//);
3743 }
3744 if (!length($args)) {
3745 print $stdout "-- illegal/missing argument\n";
3746 return 0;
3747 }
3748 print $stdout "--- warning: user list not checked\n";
3749 $return = &postjson($u,
3750 "owner_screen_name=${whoami}".
3751 "&screen_name=".&url_oauth_sub($args).
3752 "&slug=${lname}");
3753 } elsif ($comm eq 'delete' && !length($args)) {
3754 $state = "deleted list $lname";
3755 print $stdout
3756 "-- verify you want to delete list $lname\n";
3757 my $answer = lc(&linein(
3758 "-- sure you want to delete? (only y or Y is affirmative):"));
3759 if ($answer ne 'y') {
3760 print $stdout "-- ok, list is NOT deleted.\n";
3761 return 0;
3762 }
3763 $return = &postjson($delliurl,
3764 "owner_screen_name=${whoami}&slug=${lname}");
3765 if ($return) {
3766 # check and see if this is in our autolists.
3767 # if it is, delete it there too.
3768 my $value = &getvariable('lists');
3769 &setvariable('lists', $value, 1)
3770 if ($value=~s#(^|,)${whoami}/${lname}($|,)##);
3771 }
3772 } elsif ($comm eq 'list') { # synonym for /list
3773 $_ = "/list /$lname";
3774 $dont_return = 1; # and fall through
3775 } else {
3776 print $stdout "*** illegal list operation $comm\n";
3777 }
3778 if ($return) {
3779 print $stdout "*** ok, $state\n";
3780 }
3781 return 0 unless ($dont_return);
3782 }
3783
3784 # /a to show statuses in a list
3785 if (m#^/a(gain)?\s+(\+\d+\s+)?\@?([^\s/]*)/([^\s/]+)#) {
3786 my $uname = lc($3);
3787 if ($anonymous && !length($uname)) {
3788 print $stdout "-- you must specify a username when anonymous.\n";
3789 return 0;
3790 }
3791 my $lname = lc($4);
3792 my $countmaybe = $2;
3793 $countmaybe =~ s/[^\d]//g if (length($countmaybe));
3794 $countmaybe += 0;
3795 $uname ||= $whoami;
3796
3797 my $my_json_ref = &grabjson(
3798 "${statusliurl}?owner_screen_name=${uname}&slug=${lname}",
3799 0, 0, $countmaybe, undef, 1);
3800 &dt_tdisplay($my_json_ref, "again");
3801 return 0;
3802 }
3803
3804 # /lists command: if @, show their lists. if @?../... show that list.
3805 # trivially duplicates /frs and /fos for lists
3806 # also handles /listfos and /listfrs
3807 if (length($whoami) &&
3808 (m#^/list?s?$# || m#^/list?f[ro](llower|iend)?s$#)) {
3809 $_ .= " $whoami";
3810 }
3811 if (m#^/lis(t|ts|t?fos|tfollowers|t?frs|tfriends)?\s+(\+\d+\s+)?(\@?[^\s]+)$#) {
3812 my $mode = $1;
3813 my $countmaybe = $2;
3814 my $uname = lc($3);
3815 my $lname = '';
3816
3817 # Handle a case like issue 114 where you want to get a specific number of
3818 # your own lists.
3819 if ($uname =~ m/^\+/) {
3820 $countmaybe = $uname;
3821 $uname = $whoami;
3822 }
3823 $mode = ($mode =~ /^t?fo/) ? 'fo' :
3824 ($mode =~ /^t?fr/) ? 'fr' :
3825 '';
3826 $uname =~ s/^\@//;
3827 ($uname, $lname) = split(m#/#, $uname, 2) if ($uname =~ m#/#);
3828 if ($anonymous && !length($uname) && length($mode)) {
3829 print $stdout "-- you must specify a username when anonymous.\n";
3830 return 0;
3831 }
3832 $uname ||= $whoami;
3833 if (length($lname) && length($mode)) {
3834 print $stdout "-- specify username only\n";
3835 return 0;
3836 }
3837
3838 $countmaybe =~ s/[^\d]//g if (length($countmaybe));
3839 $countmaybe += 0;
3840 $countmaybe ||= 20;
3841
3842 # this is copied from /friends and /followers (q.v.)
3843 my $countper = ($countmaybe < 100) ? $countmaybe : 100;
3844
3845 my $cursor = -1; # initial value
3846 my $nofetch = 0;
3847 my $printed = 0;
3848 my $json_ref = undef;
3849 my @usarray = undef; shift(@usarray); # force underflow
3850 my $furl = (length($lname)) ? ($getliurl."?owner_")
3851 : ($mode eq '') ? ($getlisurl."?")
3852 : ($mode eq 'fo') ? ($getuliurl."?")
3853 : ($getufliurl."?");
3854 $furl .= "screen_name=${uname}";
3855 $furl .= "&slug=${lname}" if (length($lname));
3856
3857 LABIO: while($countmaybe--) {
3858 if(!scalar(@usarray)) {
3859 last LABIO if ($nofetch);
3860 $json_ref = &grabjson(
3861 "${furl}&count=${countper}&cursor=${cursor}", 0, 0, 0,
3862 undef, 1);
3863 @usarray = @{ ((length($lname)) ?
3864 $json_ref->{'users'} :
3865 $json_ref
3866 ) };
3867 last LABIO if (!scalar(@usarray));
3868 if (length($lname)) {
3869 $cursor = $json_ref->{'next_cursor_str'} ||
3870 $json_ref->{'next_cursor'} || -1;
3871 $nofetch = ($cursor < 1) ? 1 : 0;
3872 } else { $nofetch = 1; }
3873 }
3874 my $list_ref = shift(@usarray);
3875 if (length($lname)) {
3876 &$userhandle($list_ref);
3877 } else {
3878 # lists/list returns their lists AND the
3879 # ones they subscribe to, different from 1.0.
3880 # right now we just deal with that.
3881 #next if ($uname ne
3882 # $list_ref->{'user'}->{'screen_name'});
3883
3884 # listhandle?
3885 my $list_name =
3886 "\@$list_ref->{'user'}->{'screen_name'}/@{[ &descape($list_ref->{'slug'}) ]}";
3887 my $list_full_name =
3888 (length($list_ref->{'name'})) ?
3889 &descape($list_ref->{'name'})."${OFF} ($list_name)" : $list_name;
3890 my $list_mode =
3891 (lc(&descape($list_ref->{'mode'})) ne 'public') ?
3892 " ${EM}(@{[ ucfirst(&descape($list_ref->{'mode'})) ]})${OFF}" : "";
3893 print $streamout <<"EOF";
3894 ${CCprompt}$list_full_name${OFF} (f:$list_ref->{'member_count'}/$list_ref->{'subscriber_count'})$list_mode
3895 EOF
3896 my $desc = &strim(&descape($list_ref->{'description'}));
3897 my $klen = ($wrap || 79) - 9;
3898 $klen = 10 if ($klen < 0);
3899 $desc = substr($desc, 0, $klen)."..."
3900 if (length($desc) > $klen);
3901 print $streamout (' "' . $desc . '"' . "\n")
3902 if (length($desc));
3903 }
3904 $printed++;
3905 }
3906 if (!$printed) {
3907 print $stdout ((length($lname))
3908 ? "-- list $uname/$lname does not follow anyone.\n"
3909 : ($mode eq 'fr')
3910 ? "-- user $uname doesn't follow any lists.\n"
3911 : ($mode eq 'fo')
3912 ? "-- user $uname isn't followed by any lists.\n"
3913 : "-- no lists found for user $uname.\n");
3914 }
3915 return 0;
3916 }
3917
3918 &sync_n_quit if ($_ eq '/end' || $_ eq '/e');
3919
3920 #####
3921 #
3922 # below this point, we are posting
3923 #
3924 #####
3925
3926 if (m#^/me\s#) {
3927 $slash_first = 0; # kludge!
3928 }
3929
3930 if ($slash_first) {
3931 if (!m#^//#) {
3932 print $stdout "*** invalid command\n";
3933 print $stdout "*** to pass as a tweet, type /%%\n";
3934 return 0;
3935 }
3936 s#^/##; # leave the second slash on
3937 }
3938
3939 TWEETPRINT: # fugly! FUGLY!
3940 return &common_split_post($_, $quoted_status_url, $in_reply_to, undef);
3941 }
3942
3943 # this is the common code used by standard updates and by the /dm command.
3944 sub common_split_post {
3945 my $k = shift;
3946 my $quoted_status_url = shift;
3947 my $in_reply_to = shift;
3948 my $dm_user = shift;
3949
3950 my $dm_lead = (length($dm_user)) ? "/dm $dm_user " : '';
3951 my $ol = "$dm_lead$k";
3952 my $maxchars = $linelength;
3953
3954 if ($quoted_status_url) {
3955 $maxchars = $quotelinelength;
3956 }
3957 # Direct messages allegedly have no length restrictions now
3958 if ( $dm_lead ne '' || $k =~ m/^[dD] / ) {
3959 $maxchars = $dm_text_character_limit;
3960 }
3961 my (@tweetstack) = &csplit($k, $autosplit, $maxchars);
3962 my $m = shift(@tweetstack);
3963 if (scalar(@tweetstack)) {
3964 $l = "$dm_lead$m";
3965 $history[0] = $l;
3966 if (!$autosplit) {
3967 print $stdout &wwrap(
3968 "*** sorry, too long to send; ".
3969 "truncated to \"$l\" (@{[ length_newline($m) ]} chars)\n");
3970 print $stdout "*** use %% for truncated version, or append to %%.\n";
3971 return 0;
3972 }
3973 print $stdout &wwrap(
3974 "*** over $maxchars; autosplitting to \"$l\"\n");
3975 }
3976 # If a quoted status need to append that on after the length checking.
3977 if ($quoted_status_url) {
3978 $m = $m . " " . $quoted_status_url
3979 }
3980 # there was an error; stop autosplit, restore original command
3981 if (&updatest($m, 1, $in_reply_to, $dm_user)) {
3982 $history[0] = $ol;
3983 return 0;
3984 }
3985 # TODO: Perhaps also need to think about quoted tweets in the below.
3986 if (scalar(@tweetstack)) {
3987 $k = shift(@tweetstack);
3988 $l = "$dm_lead$k";
3989 &add_history($l);
3990 print $stdout &wwrap("*** next part is ready: \"$l\"\n");
3991 print $stdout "*** (this will also be automatically split)\n"
3992 if (length_newline($k) > $linelength);
3993 print $stdout
3994 "*** to send this next portion, use %%.\n";
3995 }
3996 return 1;
3997 }
3998
3999 # helper functions for the command line processor.
4000 sub add_history {
4001 my $h = shift;
4002
4003 @history = (($h, @history)[0..&min(scalar(@history), $maxhist)]);
4004 if ($termrl) {
4005 if ($termrl->Features()->{'canSetTopHistory'}) {
4006 $termrl->settophistory($h);
4007 } else {
4008 $termrl->addhistory($h);
4009 }
4010 }
4011 }
4012 sub sub_helper {
4013 my $r = shift;
4014 my $s = shift;
4015 my $g = shift;
4016 my $x;
4017 my $q = 0;
4018 my $proband;
4019
4020 if ($r eq '%') {
4021 $x = -1;
4022 } else {
4023 $x = $r + 0;
4024 }
4025 if (!$x || $x < -(scalar(@history))) {
4026 print $stdout "*** illegal history index\n";
4027 return (0, $_, undef, undef, undef);
4028 }
4029 $proband = $history[-($x + 1)];
4030 if ($s eq '--') {
4031 $q = 1;
4032 } elsif ($s eq '*') {
4033 if ($x != -1 || !length($shadow_history)) {
4034 print $stdout
4035 "*** can only %%* on most recent command\n";
4036 return (0, $_, undef, undef, undef);
4037 }
4038 # we assume it's at the end; it's only relevant there
4039 $proband = substr($shadow_history, length($g)-(2+length($r)));
4040 } else {
4041 $q = -(0+$s);
4042 }
4043 if ($q) {
4044 my $j;
4045 my $c;
4046 for($j=0; $j<$q; $j++) {
4047 $c++ if ($proband =~ s/\s+[^\s]+$//);
4048 }
4049 if ($j != $c) {
4050 print $stdout "*** illegal word index\n";
4051 return (0, $_, undef, undef, undef);
4052 }
4053 }
4054 return (1, $proband, $r, $s);
4055 }
4056
4057 # this is used for synchronicity mode to make sure we receive the
4058 # GA semaphore from the background before printing another prompt.
4059 sub sync_console {
4060 &thump;
4061 &dmthump unless (!$dmpause);
4062 }
4063 sub sync_semaphore {
4064 if ($synch) {
4065 my $k = '';
4066
4067 while(!length($k)) {
4068 sysread(W, $k, 1);
4069 } # wait for semaphore
4070 }
4071 }
4072
4073 # wrapper function to get a line from the terminal.
4074 sub linein {
4075 my $prompt = shift;
4076 my $return;
4077
4078 return 'y' if ($script);
4079
4080 $prompt .= " ";
4081 if ($termrl) {
4082 $dont_use_counter = 1;
4083 eval '$termrl->hook_no_counter';
4084 $return = $termrl->readline($prompt);
4085 $dont_use_counter = $nocounter;
4086 eval '$termrl->hook_no_counter';
4087 } else {
4088 print $stdout $prompt;
4089 chomp($return = lc(<$stdin>));
4090 }
4091 return $return;
4092 }
4093
4094 #### this is the background part of the process ####
4095
4096 MONITOR:
4097 %store_hash = ();
4098 $is_background = 1;
4099 $first_synch = $synchronous_mode = 0;
4100 $rin = '';
4101 vec($rin,fileno(STDIN),1) = 1;
4102 # paranoia
4103 binmode($stdout, ":crlf") if ($termrl);
4104 unless ($seven) {
4105 binmode(STDIN);
4106 binmode($stdout, ":utf8");
4107 }
4108
4109 # allow foreground process to squelch us
4110 # we have to cover all the various versions of 30/31 signals on various
4111 # systems just in case we are on a system without POSIX.pm. this set should
4112 # cover Linux 2.x/3.x, AIX, Mac OS X, *BSD and Solaris. we have to assert
4113 # these signals before starting streaming, or we may "kill" ourselves by
4114 # accident because it is possible to process a tweet before these are
4115 # operational.
4116 &sigify(sub {
4117 $suspend_output ^= 1 if ($suspend_output != -1);
4118 $we_got_signal = 1;
4119 }, qw(USR1 PWR XCPU));
4120 &sigify( sub {
4121 $suspend_output = -1; $we_got_signal = 1;
4122 }, qw(USR2 SYS UNUSED XFSZ));
4123 &sigify("IGNORE", qw(INT)); # don't let slowpost kill us
4124
4125 # now we can safely initialize streaming
4126 if ($dostream) {
4127 @events = ();
4128 $lasteventtime = time();
4129 &sigify(sub {
4130 print $stdout "-- killing processes $nursepid $bufferpid\n"
4131 if ($verbose);
4132 kill $SIGHUP, $nursepid if ($nursepid);
4133 kill $SIGHUP, $bufferpid if ($bufferpid);
4134 kill 9, $curlpid if ($curlpid);
4135 sleep 1;
4136 # send myself a shutdown
4137 kill 9, $nursepid if ($nursepid);
4138 kill 9, $bufferpid if ($bufferpid);
4139 kill $SIGTERM, $$;
4140 }, qw(HUP)); # use SIGHUP etc. from parent process to signal end
4141 $bufferpid = &start_streaming;
4142 vec($rin, fileno(STBUF), 1) = 1;
4143 } else {
4144 &sigify("IGNORE", qw(HUP)); # we only respond to SIGKILL/SIGTERM
4145 }
4146
4147 $interactive = $previous_last_id = $we_got_signal = 0;
4148 $hold = 0;
4149 $suspend_output = -1;
4150 $stream_failure = 0;
4151 $dm_first_time = ($dmpause) ? 1 : 0;
4152 $stuck_stdin = 0;
4153
4154 # tell the foreground we are ready
4155 kill $SIGUSR2, $parent;
4156
4157 # loop until we are killed or told to stop.
4158 # we receive instructions on stdin, and send data back on our pipe().
4159 for(;;) {
4160 &$heartbeat;
4161 &update_effpause;
4162 $wrapseq = 0; # remember, we don't know when commands are sent.
4163 &refresh($interactive, $previous_last_id) unless
4164 (!$effpause && !$interactive);
4165 $dont_refresh_first_time = 0;
4166 $previous_last_id = $last_id;
4167 if ($dmpause && ($effpause || $synch)) {
4168 if ($dm_first_time) {
4169 &dmrefresh(0);
4170 $dmcount = $dmpause;
4171 } elsif (!$interactive) {
4172 if (!--$dmcount) {
4173 &dmrefresh($interactive); # using dm_first_time
4174 $dmcount = $dmpause;
4175 }
4176 }
4177 }
4178 DONT_REFRESH:
4179 # nrvs is tricky with synchronicity
4180 if (!$synch || ($synch && $synchronous_mode && !$dm_first_time)) {
4181 $k = length($notify_rate) + length($vs) + length($credlog);
4182 if ($k) {
4183 &send_removereadline if ($termrl);
4184 print $stdout $notify_rate;
4185 print $stdout $vs;
4186 print $stdout $credlog;
4187 $wrapseq = 1;
4188 }
4189 $notify_rate = "";
4190 $vs = "";
4191 $credlog = "";
4192 }
4193 print P "0" if ($synchronous_mode && $interactive);
4194 &send_repaint if ($termrl);
4195
4196 # this core loop is tricky. most signals will not restart the call.
4197 # -- respond to alarms if we are ignoring our timeout.
4198 # -- do not respond to bogus packets if a signal handler triggered it.
4199 # -- clear our flag when we detect a signal handler has been called.
4200
4201 # if our master select is interrupted, we must restart with the
4202 # appropriate time taken from effpause. however, most implementations
4203 # don't report timeleft, so we must.
4204 $restarttime = time() + $effpause;
4205 RESTART_SELECT:
4206 &send_repaint if ($termrl);
4207 $interactive = 0;
4208 $we_got_signal = 0; # acknowledge all signals
4209 if ($effpause == undef) { # -script and anonymous have no effpause.
4210 print $stdout "-- select() loops forever\n" if ($verbose);
4211 $nfound = select($rout = $rin, undef, undef, undef);
4212 } else {
4213 $actualtime = $restarttime - time();
4214 print $stdout "-- select pending ($actualtime sec left)\n"
4215 if ($superverbose);
4216 if ($actualtime <= 0) {
4217 $nfound = 0;
4218 } else {
4219 $nfound = select(
4220 $rout = $rin, undef, undef, $actualtime);
4221 }
4222 }
4223 if ($nfound > 0) {
4224 my $len;
4225
4226 # service the streaming socket first, if we have one.
4227 if ($dostream) {
4228 if (vec($rout, fileno(STBUF), 1) == 1) {
4229 my $json_ref;
4230 my $buf = '';
4231 my $rbuf;
4232 my $reads = 0;
4233
4234 print $stdout "-- data on streaming socket\n"
4235 if ($superverbose);
4236
4237 # read until we get eight hex digits. this forces the
4238 # data stream to synchronize.
4239 # first, however, make sure we actually have valid
4240 # data, or we sit here and slow down the user.
4241 read(STBUF, $buf, 1);
4242 if (!length($buf)) {
4243 # if we get a "ready" but there's actually
4244 # no data, that means either 1) a signal
4245 # occurred on the buffer, which we need to
4246 # ignore, or 2) something killed the
4247 # buffer, which is unrecoverable. if we keep
4248 # getting repeated ready-no data situations,
4249 # it's probably the latter.
4250 $stream_failure++;
4251 &screech(<<"EOF") if ($stream_failure > 100);
4252
4253 *** fatal error ***
4254 something killed the streaming buffer process. I can't recover from this.
4255 please restart oysttyer.
4256 EOF
4257 goto DONESTREAM;
4258 }
4259 $stream_failure = 0;
4260 if ($buf !~ /^[0-9a-fA-F]+$/) {
4261 print $stdout
4262 "-- warning: bogus character(s) ".unpack("H*", $buf)."\n"
4263 if ($superverbose);
4264 goto DONESTREAM;
4265 }
4266 while (length($buf) < 8) {
4267 # don't read 8 -- read 1. that means we can
4268 # skip trailing garbage without a window.
4269 read(STBUF, $rbuf, 1);
4270 $reads++;
4271 if ($rbuf =~ /[0-9a-fA-F]/) {
4272 $buf .= $rbuf;
4273 $reads = 0;
4274 } else {
4275 print $stdout
4276 "-- warning: bogus character(s) ".unpack("H*", $rbuf)."\n"
4277 if ($superverbose);
4278 $buf = ''
4279 if (length($rbuf)); # bogus data
4280 }
4281 print $stdout
4282 "-- master, I am stuck: $reads reads on stream and no valid data\n"
4283 if ($reads > 0 && ($reads % 1000) == 0);
4284 }
4285 print $stdout "-- length packet: $buf\n"
4286 if ($superverbose);
4287 $len = hex($buf);
4288 $buf = '';
4289 while (length($buf) < $len) {
4290 read(STBUF, $rbuf, ($len-length($buf)));
4291 $buf .= $rbuf;
4292 }
4293
4294 print $stdout
4295 "-- streaming data ($len) --\n$buf\n-- streaming data --\n\n"
4296 if ($superverbose);
4297 $json_ref = &parsejson($buf);
4298 push(@events, $json_ref);
4299
4300 if (scalar(@events) > $eventbuf || (scalar(@events) &&
4301 (time()-$lasteventtime) > $effpause)){
4302 sleep 5 while ($suspend_output > 0);
4303 &streamevents(@events);
4304 &send_repaint if ($termrl);
4305 @events = ();
4306 $lasteventtime = time();
4307 }
4308 }
4309 DONESTREAM: print $stdout "-- done with streaming events\n"
4310 if ($superverbose);
4311 }
4312
4313 # then, check if there is data on our control socket.
4314 # command packets should always be (initially) 20 characters.
4315 # if we come up short, it's either a bug, signal or timeout.
4316 if ($we_got_signal) {
4317 goto RESTART_SELECT;
4318 }
4319 goto RESTART_SELECT if(vec($rout, fileno(STDIN), 1) != 1);
4320 print $stdout "-- waiting for data ", scalar localtime, "\n"
4321 if ($superverbose);
4322 if(sysread(STDIN, $rout, 20) != 20) {
4323 # if we get repeated "ready" but no data on STDIN,
4324 # like the streaming buffer, we probably lost our
4325 # IPC and we should die here.
4326 if (++$stuck_stdin > 100) {
4327 print $stdout "parent is dead; we die too\n";
4328 kill 9,$$;
4329 }
4330 goto RESTART_SELECT;
4331 }
4332 $stuck_stdin = 0;
4333 # background communications central command code
4334 # we received a command from the console, so let's look at it.
4335 print $stdout "-- command received ", scalar
4336 localtime, " $rout" if ($verbose);
4337 if ($rout =~ /^hold/) {
4338 $holdhold ^= 1; # toggle hold flag
4339 goto RESTART_SELECT;
4340 } elsif ($rout =~ /^rsga/) {
4341 $suspend_output = 0; # reset our status
4342 goto RESTART_SELECT;
4343 } elsif ($rout =~ /^pipet (..)/) {
4344 my $key = &get_tweet($1);
4345 my $ms = $key->{'menu_select'} || 'XX';
4346 my $ds = $key->{'created_at'} || 'argh, no created_at';
4347 $ds =~ s/\s/_/g;
4348 my $src = $key->{'source'} || 'unknown';
4349 # Figured out this is where the stream gets processed and oysttyer picks out the fields that get stored
4350 # So quoted_status_id_str needed adding in here.
4351 $src =~ s/\|//g; # shouldn't be any anyway.
4352 $key = substr(( join "\0", $ms, $key->{'id_str'},
4353 $key->{'in_reply_to_status_id_str'},
4354 $key->{'quoted_status_id_str'},
4355 $key->{'quoted_status'}->{'text'},
4356 $key->{'quoted_status'}->{'full_text'},
4357 $key->{'quoted_status'}->{'extended_tweet'}->{'full_text'},
4358 $key->{'retweeted_status'}->{'id_str'},
4359 $key->{'retweeted_status'}->{'text'},
4360 $key->{'retweeted_status'}->{'full_text'},
4361 $key->{'retweeted_status'}->{'extended_tweet'}->{'full_text'},
4362 $key->{'retweeted_status'}->{'quoted_status'}->{'id_str'},
4363 $key->{'retweeted_status'}->{'quoted_status'}->{'text'},
4364 $key->{'retweeted_status'}->{'quoted_status'}->{'full_text'},
4365 $key->{'retweeted_status'}->{'quoted_status'}->{'extended_tweet'}->{'full_text'},
4366 $key->{'user'}->{'geo_enabled'} || "false",
4367 $key->{'geo'}->{'coordinates'}->[0],
4368 $key->{'geo'}->{'coordinates'}->[1],
4369 $key->{'place'}->{'id'},
4370 $key->{'place'}->{'country_code'},
4371 $key->{'place'}->{'place_type'},
4372 unpack("${pack_magic}H*", $key->{'place'}->{'full_name'}),
4373 $key->{'tag'}->{'type'},
4374 unpack("${pack_magic}H*", $key->{'tag'}->{'payload'}),
4375 $key->{'retweet_count'} || "0",
4376 $key->{'user'}->{'screen_name'}, $ds, $src,
4377 unpack("${pack_magic}H*", $key->{'text'}).
4378 $space_pad), 0, $packet_length);
4379 print P $key;
4380 goto RESTART_SELECT;
4381 } elsif ($rout =~ /^piped (..)/) {
4382 my $key = $dm_store_hash{$1};
4383 my $ms = $key->{'menu_select'} || 'XX';
4384 my $ds = $key->{'created_at'} || 'argh, no created_at';
4385 $ds =~ s/\s/_/g;
4386 $key = substr(( "$ms ".($key->{'id_str'})." ".
4387 $key->{'sender'}->{'screen_name'}." $ds ".
4388 unpack("${pack_magic}H*", $key->{'text'}).
4389 $space_pad), 0, $packet_length);
4390 print P $key;
4391 goto RESTART_SELECT;
4392 } elsif ($rout =~ /^ki ([^\s]+) /) {
4393 my $key = $1;
4394 my $module;
4395 read(STDIN, $module, $packet_length);
4396 $module =~ s/\s+$//;
4397 $module = pack("H*", $module);
4398 print $stdout "-- fetch for module $module key $key\n"
4399 if ($verbose);
4400 print P substr(unpack("${pack_magic}H*",
4401 $master_store->{$module}->{$key}).$space_pad,
4402 0, $packet_length);
4403 goto RESTART_SELECT;
4404 } elsif ($rout =~ /^kn ([^\s]+) /) {
4405 my $key = $1;
4406 my $module;
4407 read(STDIN, $module, $packet_length);
4408 $module =~ s/\s+$//;
4409 $module = pack("H*", $module);
4410 print $stdout "-- nulled module $module key $key\n"
4411 if ($verbose);
4412 $master_store->{$module}->{$key} = undef;
4413 goto RESTART_SELECT;
4414 } elsif ($rout =~ /^ko ([^\s]+) /) {
4415 my $key = $1;
4416 my $value;
4417 my $module;
4418 read(STDIN, $module, $packet_length);
4419 $module =~ s/\s+$//;
4420 $module = pack("H*", $module);
4421 read(STDIN, $value, $packet_length);
4422 $value =~ s/\s+$//;
4423 print $stdout
4424 "-- set module $module key $key = $value\n"
4425 if ($verbose);
4426 $master_store->{$module}->{$key} = pack("H*", $value);
4427 goto RESTART_SELECT;
4428 } elsif ($rout =~ /^sync/) {
4429 print $stdout "-- synced; exiting at ",
4430 scalar localtime, "\n"
4431 if ($verbose);
4432 exit $laststatus;
4433 } elsif ($rout =~ /^synm/) {
4434 $first_synch = $synchronous_mode = 1;
4435 print $stdout "-- background is now synchronous\n"
4436 if ($verbose);
4437 } elsif ($rout =~ /([\=\?\+])([^ ]+)/) {
4438 $comm = $1;
4439 $key =$2;
4440 if ($comm eq '?') {
4441 print P substr("${$key}$space_pad", 0, $packet_length);
4442 } else {
4443 read(STDIN, $value, $packet_length);
4444 $value =~ s/\s+$//;
4445 $interactive = ($comm eq '+') ? 0 : 1;
4446 if ($key eq 'tquery') {
4447 print $stdout
4448 "*** custom query installed\n"
4449 if ($interactive || $verbose);
4450 print $stdout
4451 "$value" if ($verbose);
4452 @trackstrings = ();
4453 # already URL encoded
4454 push(@trackstrings, $value);
4455 } else {
4456 $$key = $value;
4457 print $stdout
4458 "*** changed: $key => $$key\n"
4459 if ($interactive || $verbose);
4460
4461 &generate_ansi if ($key eq 'ansi' ||
4462 $key =~ /^colour/);
4463 $rate_limit_next = 0
4464 if ($key eq 'pause' &&
4465 $value eq 'auto');
4466 &tracktags_makearray
4467 if ($key eq 'track');
4468 &filter_compile
4469 if ($key eq 'filter');
4470 &notify_compile
4471 if ($key eq 'notifies');
4472 &list_compile
4473 if ($key eq 'lists');
4474 &filterflags_compile
4475 if ($key eq 'filterflags');
4476 $filterrts_sub =
4477 &filteruserlist_compile(
4478 $filterrts_sub, $value)
4479 if ($key eq 'filterrts');
4480 $filterusers_sub =
4481 &filteruserlist_compile(
4482 $filterusers_sub,$value)
4483 if ($key eq 'filterusers');
4484 $filteratonly_sub =
4485 &filteruserlist_compile(
4486 $filteratonly_sub,
4487 $value)
4488 if ($key eq 'filteratonly');
4489 &filterats_compile
4490 if ($key eq 'filterats');
4491 }
4492 }
4493 goto RESTART_SELECT;
4494 } else {
4495 $interactive = 1;
4496 ($fetchwanted = 0+$1, $fetch_id = 0, $last_id = 0)
4497 if ($rout =~ /^reset(\d+)/);
4498 ($dmfetchwanted = 0+$1, $last_dm = 0)
4499 if ($rout =~ /^dmreset(\d+)/);
4500 if ($rout =~ /^smreset/) { # /dmsent
4501 $dmfetchwanted = 0+$1
4502 if ($rout =~ /(\d+)/);
4503 &dmrefresh(1, 1);
4504 &send_repaint if ($termrl);
4505 # we do not want to force a refresh.
4506 goto DONT_REFRESH;
4507 }
4508 if ($rout =~ /^dm/) {
4509 &dmrefresh($interactive);
4510 &send_repaint if ($termrl);
4511 $dmcount = $dmpause;
4512 goto DONT_REFRESH;
4513 }
4514 }
4515 } else {
4516 if ($we_got_signal || $nfound == -1 || $holdhold) {
4517 # we need to restart the call. we might be waiting
4518 # longer, but this is unavoidable.
4519 goto RESTART_SELECT;
4520 }
4521 print $stdout
4522 "-- routine refresh (effpause = $effpause, $dmcount to next dm) ",
4523 scalar localtime, "\n" if ($verbose);
4524 }
4525 }
4526
4527 #### internal implementation functions for the twitter API. DON'T ALTER ####
4528
4529 # manage automatic rate limiting by checking our max.
4530 #TODO
4531 # autoslowdown as we run out of requests, then speed up when hour
4532 # has passed.
4533 sub update_effpause {
4534 return ($effpause = undef) if ($script); # for select()
4535 if ($pause ne 'auto' && $noratelimit) {
4536 $effpause = (0+$pause) || undef;
4537 return;
4538 }
4539 $effpause = (0+$pause) || undef
4540 if ($anonymous || (!$pause && $pause ne 'auto'));
4541 if (!$rate_limit_next && !$anonymous && ($pause > 0 ||
4542 $pause eq 'auto')) {
4543
4544 # Twitter 1.0 used a simple remaining_hits and
4545 # hourly_limit. 1.1 uses multiple rate endpoints. we
4546 # are only interested in certain specific ones, though
4547 # we currently fetch them all and we might use more later.
4548
4549 $rate_limit_next = 5;
4550 $rate_limit_ref = &grabjson($rlurl, 0, 0, 0, undef, 1);
4551
4552 if (defined $rate_limit_ref &&
4553 ref($rate_limit_ref) eq 'HASH') {
4554
4555 # of mentions_timeline, home_timeline and search/tweets,
4556 # choose the MOST restrictive and normalize that.
4557
4558 $rate_limit_left = &min(
4559 0+$rate_limit_ref->{'resources'}->{'statuses'}->{'\\/statuses\\/home_timeline'}->{'remaining'},
4560 &min(
4561 0+$rate_limit_ref->{'resources'}->{'statuses'}->{'\\/statuses\\/mentions_timeline'}->{'remaining'},
4562 0+$rate_limit_ref->{'resources'}->{'search'}->{'\\/search\\/tweets'}->{'remaining'}));
4563 $rate_limit_rate = &min(
4564 0+$rate_limit_ref->{'resources'}->{'statuses'}->{'\\/statuses\\/home_timeline'}->{'limit'},
4565 &min(
4566 0+$rate_limit_ref->{'resources'}->{'statuses'}->{'\\/statuses\\/mentions_timeline'}->{'limit'},
4567 0+$rate_limit_ref->{'resources'}->{'search'}->{'\\/search\\/tweets'}->{'limit'}));
4568 if ($rate_limit_left < 3 && $rate_limit_rate) {
4569 $estring =
4570 "*** warning: API rate limit imminent";
4571 if ($pause eq 'auto') {
4572 $estring .=
4573 "; temporarily halting autofetch";
4574 $effpause = 0;
4575 }
4576 &$exception(5, "$estring\n");
4577 } else {
4578 if ($pause eq 'auto') {
4579
4580 # the new rate limits do not require us to reduce our fetching for mentions,
4581 # direct messages or search, because they pull from different buckets, and
4582 # their rate limits are roughly the same.
4583 $effpause = 5*$rate_limit_rate;
4584 # this will usually be 75s
4585 # for lists, however, we have to drain the list bucket faster, so for every
4586 # list AFTER THE FIRST ONE we subscribe to, add rate_limit_rate to slow.
4587 # for search, it has 180 requests, so we don't care so much. if this
4588 # changes later, we will probably need something similar to this for
4589 # cases where the search array is > 1.
4590 $effpause += ((scalar(@listlist)-1)*
4591 $rate_limit_rate)
4592 if (scalar(@listlist) > 1);
4593
4594 if (!$effpause) {
4595 print $stdout
4596 "-- rate limit rate failure: using 180 second fallback\n";
4597 $effpause = 180;
4598 }
4599
4600 # we don't go under sixty.
4601 $effpause = 60 if ($effpause < 60);
4602 } else {
4603 $effpause = 0+$pause;
4604 }
4605 }
4606 print $stdout
4607 "-- rate limit check: $rate_limit_left/$rate_limit_rate (rate is $effpause sec)\n"
4608 if ($verbose);
4609 $adverb = (!$last_rate_limit) ? ' currently' :
4610 ($last_rate_limit < $rate_limit_rate) ? ' INCREASED to':
4611 ($last_rate_limit > $rate_limit_rate) ? ' REDUCED to':
4612 '';
4613 $notify_rate =
4614 "-- notification: API rate limit is${adverb} ${rate_limit_rate} req/15min\n"
4615 if ($last_rate_limit != $rate_limit_rate);
4616 $last_rate_limit = $rate_limit_rate;
4617 } else {
4618 $rate_limit_next = 0;
4619 $effpause = ($pause eq 'auto') ? 180 : 0+$pause;
4620 print $stdout
4621 "-- failed to fetch rate limit (rate is $effpause sec)\n"
4622 if ($verbose);
4623 }
4624 } else {
4625 $rate_limit_next-- unless ($anonymous);
4626 }
4627 }
4628
4629 # streaming API support routines
4630
4631 ### INITIALIZE STREAMING
4632 ### spin off a nurse process to proxy data from curl, and a buffer process
4633 ### to protect the background process from signals curl may generate.
4634
4635 sub start_streaming {
4636 $bufferpid = 0;
4637 unless ($streamtest) {
4638 if($bufferpid = open(STBUF, "-|")) {
4639 # streaming processes initialized
4640 return $bufferpid;
4641 }
4642 }
4643
4644 # now within buffer process
4645 # verbosity does not work here, so force both off.
4646 $verbose = 0;
4647 $superverbose = 0;
4648
4649 $0 = "oysttyer (streaming buffer thread)";
4650 $in_buffer = 1;
4651 # set up signal handlers
4652 $streampid = 0;
4653 &sigify(sub {
4654 # in an earlier version we wrote a disconnect packet to the
4655 # pipe in this handler. THIS IS NOT SAFE on certain OS/Perl
4656 # combinations. I moved this down to the HELLOAGAINNURSE loop,
4657 # or otherwise you get random seg faults.
4658 $i = $streampid;
4659 $streampid = 0;
4660 waitpid $i, 0 if ($i);
4661 }, qw(CHLD PIPE));
4662 &sigify(sub {
4663 $i = $streampid;
4664 $streampid = 0; # suppress handler above
4665 kill ($SIGHUP, $i) if ($i);
4666 waitpid $i, 0 if ($i);
4667 kill 9, $curlpid if ($curlpid && !$i);
4668 kill 9, $$;
4669 }, qw(HUP TERM));
4670 &sigify("IGNORE", qw(INT));
4671
4672 $packets_read = 0; # part of exponential backoff
4673 $wait_time = 0;
4674
4675 # open the nurse process
4676 HELLOAGAINNURSE: $w = "{\"packet\" : \"connect\", \"payload\" : {} }";
4677 select(STDOUT); $|++;
4678 printf STDOUT ("%08x%s", length($w), $w);
4679 close(NURSE);
4680 if (!$packets_read) { $wait_time += (($wait_time) ? $wait_time : 1) }
4681 else { $wait_time = 0; }
4682 $packets_read = 0;
4683 $wait_time = ($wait_time > 60) ? 60 : $wait_time;
4684 if ($streampid = open(NURSE, "-|")) {
4685 # within the buffer process
4686 select(NURSE); $|++; select(STDOUT);
4687 my $rin = '';
4688 vec($rin,fileno(NURSE),1) = 1;
4689 my $datasize = 0;
4690 my $buf = '';
4691 my $cuf = '';
4692 my $duf = '';
4693
4694 # read the curlpid from the stream
4695 read(NURSE, $curlpax, 8);
4696 $curlpid = hex($curlpax);
4697
4698 # if we are testing the socket, just emit data.
4699 if ($streamtest) {
4700 my $c;
4701
4702 for(;;) {
4703 read(NURSE, $c, 1);
4704 print STDOUT $c;
4705 }
4706 }
4707 HELLONURSE: while(1) {
4708 # restart nurse process if it/curl died
4709 goto HELLOAGAINNURSE if(!$streampid);
4710
4711 # read a line of text (hopefully numbers)
4712 chomp($buf = <NURSE>);
4713 # should be nothing but digits and whitespace.
4714 # if anything else, we're getting garbage, and we
4715 # should reconnect.
4716 if ($buf =~ /[^0-9\r\l\n\s]+/s) {
4717 close(NURSE);
4718 kill 9, $streampid if ($streampid);
4719 # and SIGCHLD will reap
4720 kill 9, $curlpid if ($curlpid);
4721 goto HELLOAGAINNURSE;
4722 }
4723 $datasize = 0+$buf;
4724 next HELLONURSE if (!$datasize);
4725 $datasize--;
4726 read(NURSE, $duf, $datasize);
4727 # don't send broken entries
4728 next HELLONURSE if (length($duf) < $datasize);
4729 # yank out all \r\n
4730 1 while $duf =~ s/[\r\n]//g;
4731 $duf = "{ \"packet\" : \"data\", \"pid\" : \"$streampid\", \"curlpid\" : \"$curlpid\", \"payload\" : $duf }";
4732 printf STDOUT ("%08x%s", length($duf), $duf);
4733 $packets_read++;
4734 }
4735 } else {
4736 # within the nurse process
4737 $0 = "oysttyer (waiting $wait_time sec to connect to stream)";
4738 sleep $wait_time;
4739 $curlpid = 0;
4740 $replarg = ($streamallreplies) ? '&replies=all' : '';
4741 &sigify(sub {
4742 kill 9, $curlpid if ($curlpid);
4743 waitpid $curlpid, 0 unless (!$curlpid);
4744 $curlpid = 0;
4745 kill 9, $$;
4746 }, qw(CHLD PIPE));
4747 &sigify(sub {
4748 kill 9, $curlpid if ($curlpid);
4749 }, qw(INT HUP TERM)); # which will cascade into SIGCHLD
4750 ($comm, $args, $data) = &$stringify_args($baseagent,
4751 [ $streamurl, "delimited=length${replarg}" ],
4752 undef, undef,
4753 '-s',
4754 '-A', "oysttyer_Streaming/$oysttyer_VERSION",
4755 '-N',
4756 '-H', 'Expect:');
4757 ($curlpid = open(K, "|$comm")) || die("failed curl: $!\n");
4758 printf STDOUT ("%08x", $curlpid);
4759
4760 # "DIE QUICKLY"
4761 $0 = "oysttyer (streaming socket nurse thread to ${curlpid})";
4762
4763 select(K); $|++; select(STDOUT); $|++;
4764 print K "$args\n";
4765 close(K);
4766 waitpid $curlpid, 0;
4767 $curlpid = 0;
4768 kill 9, $$;
4769 }
4770 }
4771
4772 # handle a set of events acquired from the streaming socket.
4773 # ordinarily only the background is calling this.
4774 sub streamevents {
4775 my (@events) = (@_);
4776 my $w;
4777 my @x;
4778 my %k; # need temporary dedupe
4779
4780 foreach $w (@events) {
4781 my $tmp;
4782
4783 # don't send non-data events (yet).
4784 next if ($w->{'packet'} ne 'data');
4785
4786 # try to get PID information if available for faster shutdown
4787 $nnursepid = 0+($w->{'pid'});
4788 if ($nnursepid != $nursepid) {
4789 $nursepid = $nnursepid;
4790 print $stdout
4791 "-- got new pid of streaming nurse socket process: $nursepid\n"
4792 if ($verbose);
4793 }
4794 $ncurlpid = 0+($w->{'curlpid'});
4795 if ($ncurlpid != $curlpid) {
4796 $curlpid = $ncurlpid;
4797 print $stdout
4798 "-- got new pid of streaming curl process: $ncurlpid\n"
4799 if ($verbose);
4800 }
4801
4802 # we don't use this (yet).
4803 next if ($w->{'payload'}->{'friends'});
4804
4805 sleep 5 while ($suspend_output > 0);
4806
4807 # dispatch tweets
4808 if ($w->{'payload'}->{'text'} && !$notimeline) {
4809 # normalize the tweet first.
4810 my $payload = &normalizejson($w->{'payload'});
4811 my $sid = $payload->{'id_str'};
4812
4813 $payload->{'tag'}->{'type'} = 'timeline';
4814 $payload->{'tag'}->{'payload'} = 'stream';
4815
4816 # filter replies from streaming socket if the
4817 # user requested it. use $tweettype to determine
4818 # this so the user can interpose custom logic.
4819 if ($nostreamreplies) {
4820 my $sn = &descape(
4821 $payload->{'user'}->{'screen_name'});
4822 my $text = &descape($payload->{'text'});
4823 next if (&$tweettype($payload, $sn, $text) eq
4824 'reply');
4825 }
4826
4827 # finally, filter everything else and dedupe.
4828 unless (length($id_cache{$sid}) ||
4829 $filter_next{$sid} ||
4830 $k{$sid}) {
4831 &tdisplay([ $payload ]);
4832 $k{$sid}++;
4833 }
4834
4835 # roll *_id so that we don't do unnecessary work
4836 # testing the API. don't roll fetch_id, search uses
4837 # it. don't roll if last_id was zero, because that
4838 # means we are streaming *before* the API backfetch.
4839 $last_id = $sid unless (!$last_id);
4840 }
4841
4842 # dispatch DMs
4843 elsif (($tmp = $w->{'payload'}->{'direct_message'}) &&
4844 $dmpause) {
4845 &dmrefresh(0, 0, [ $tmp ]);
4846 # don't roll last_dm yet.
4847 }
4848
4849 # must be an event. see if standardevent can make sense of it.
4850 elsif (!$notimeline) {
4851 $w = $w->{'payload'};
4852 my $sou_sn =
4853 &descape($w->{'source'}->{'screen_name'});
4854 if (!length($sou_sn) || !$filterusers_sub ||
4855 !&$filterusers_sub($sou_sn)) {
4856 &send_removereadline if ($termrl);
4857 &$eventhandle($w);
4858 $wrapseq = 1;
4859 &send_repaint if ($termrl);
4860 }
4861 }
4862 }
4863 }
4864
4865 # REST API support
4866 #
4867 # thump for timeline
4868 # THIS MUST ONLY BE RUN BY THE BACKGROUND.
4869 sub refresh {
4870 my $interactive = shift;
4871 my $relative_last_id = shift;
4872 my $k;
4873 my $my_json_ref = undef;
4874 my $i;
4875 my @streams = ();
4876 my $dont_roll_back_too_far = 0;
4877
4878 # this mixes all the tweet streams (timeline, hashtags, replies
4879 # and lists) into a single unified data river.
4880 # backload can be zero, but this will still work since &grabjson
4881 # sees a count of zero as "default."
4882
4883 # first, get my own timeline
4884 # note that anonymous has no timeline (but they can sample the
4885 # stream)
4886 unless ($notimeline || $anonymous) {
4887 # in streaming mode, use $last_id
4888 # in API mode, use $fetch_id
4889 my $base_json_ref = &grabjson($url,
4890 ($dostream) ? $last_id : $fetch_id,
4891 0,
4892 (($last_id) ? 250 : $fetchwanted || $backload), {
4893 "type" => "timeline",
4894 "payload" => "api"
4895 }, 1);
4896 # if I can't get my own timeline, ABORT! highest priority!
4897 return if (!defined($base_json_ref) ||
4898 ref($base_json_ref) ne 'ARRAY');
4899
4900 # we have to filter against the ID cache right now, because
4901 # we might not have any other streams!
4902 if ($fetch_id && $last_id) {
4903 $my_json_ref = [];
4904 my $l;
4905 my %k; # need temporary dedupe
4906 foreach $l (@{ $base_json_ref }) {
4907 unless (length($id_cache{$l->{'id_str'}}) ||
4908 $filter_next{$l->{'id_str'}} ||
4909 $k{$l->{'id_str'}}) {
4910 push(@{ $my_json_ref }, $l);
4911 $k{$l->{'id_str'}}++;
4912 }
4913 }
4914 } else {
4915 $my_json_ref = $base_json_ref;
4916 }
4917 }
4918
4919 # add stream for replies, if requested
4920 if ($mentions) {
4921 # same thing
4922 my $r = &grabjson($rurl,
4923 ($dostream && !$nostreamreplies) ? $last_id : $fetch_id,
4924 0,
4925 (($last_id) ? 250
4926 : $fetchwanted || $backload), {
4927 "type" => "reply",
4928 "payload" => ""
4929 }, 1);
4930 push(@streams, $r)
4931 if (defined($r) &&
4932 ref($r) eq 'ARRAY' &&
4933 scalar(@{ $r }));
4934 }
4935
4936 # next handle hashtags and tracktags
4937 # failure here does not abort, because search may be down independently
4938 # of the main timeline.
4939 if (!$notrack && scalar(@trackstrings)) {
4940 my $r;
4941 my $k;
4942 my $l;
4943
4944 if (!$last_id) {
4945 $l = &min($backload, $searchhits);
4946 } else {
4947 $l = (($fetchwanted) ? $fetchwanted :
4948 &max(100, $searchhits));
4949 }
4950 # temporarily squelch server complaints (see below)
4951 $muffle_server_messages = 1 unless ($verbose);
4952 foreach $k (@trackstrings) {
4953 # use fetch_id here in both modes.
4954 $r = &grabjson("$queryurl?${k}&result_type=recent",
4955 $fetch_id, 0, $l, {
4956 "type" => "search",
4957 "payload" => $k
4958 }, 1);
4959 # depending on the state of the search API, we might be using
4960 # a bogus search ID that is too far back. so if this fails,
4961 # try again with last_id, but not if we're streaming (it
4962 # will always fetch zero).
4963 if (!defined($r) || ref($r) ne 'ARRAY' || !$dostream) {
4964 print $stdout "-- search retry $k attempted with last_id\n"
4965 if ($verbose);
4966 $r = &grabjson("$queryurl?${k}&result_type=recent",
4967 $last_id, 0, $l, {
4968 "type" => "search",
4969 "payload" => $k
4970 }, 1);
4971 $dont_roll_back_too_far = 1;
4972 }
4973 # or maybe not even then?
4974 if (!defined($r) || ref($r) ne 'ARRAY') {
4975 print $stdout "-- search retry $k attempted with zero!\n"
4976 if ($verbose);
4977 $r = &grabjson("$queryurl?${k}&result_type=recent",
4978 0, 0, $l, {
4979 "type" => "search",
4980 "payload" => $k
4981 }, 1);
4982 $dont_roll_back_too_far = 1;
4983 }
4984 push(@streams, $r)
4985 if (defined($r) &&
4986 ref($r) eq 'ARRAY' &&
4987 scalar(@{ $r }));
4988 }
4989 $muffle_server_messages = 0;
4990 }
4991
4992 # add stream for lists we have on with /set lists, and tag it with
4993 # the list.
4994 if (scalar(@listlist)) {
4995 foreach $k (@listlist) {
4996 # always use fetch_id
4997 my $r = &grabjson(
4998 "${statusliurl}?owner_screen_name=".$k->[0].'&slug='.$k->[1],
4999 $fetch_id, 0,
5000 (($last_id) ? 250 : $fetchwanted), {
5001 "type" => "list",
5002 "payload" => ($k->[0] ne $whoami) ?
5003 "$k->[0]/$k->[1]" :
5004 "$k->[1]"
5005 }, 1);
5006 push(@streams, $r)
5007 if (defined($r) && ref($r) eq 'ARRAY' &&
5008 scalar(@{ $r }));
5009 }
5010 }
5011
5012 $fetchwanted = 0; # done with that.
5013 # now, streamix all the streams into my_json_ref, discarding duplicates
5014 # a simple hash lookup is no good; it has to be iterative. because of
5015 # that, we might as well just splice it in here and save a sort later.
5016 # the streammix logic is unnecessarily complex, probably.
5017 # remember, the most recent tweets are FIRST.
5018 if (scalar(@streams)) {
5019 my $j;
5020 my $k;
5021 my $l = scalar(@{ $my_json_ref });
5022 my $m;
5023 my $n;
5024
5025 foreach $n (@streams) {
5026 SMIX0: foreach $j (@{ $n }) {
5027 my $id = $j->{'id_str'}; # for ease of use
5028 # possible to happen if search tryhard is on
5029 next SMIX0 if ($id < $fetch_id);
5030
5031 # filter this lot against the id cache
5032 # and any tweets we just filtered.
5033 next SMIX0 if (length($id_cache{$id}) &&
5034 $fetch_id);
5035 next SMIX0 if ($filter_next{$id} &&
5036 $fetch_id);
5037
5038 if (!$l) { # degenerate case
5039 push (@{ $my_json_ref }, $j);
5040 $l++;
5041 next SMIX0;
5042 }
5043
5044 # find the same ID, or one just before,
5045 # and splice in
5046 $m = -1;
5047 SMIX1: for($i=0; $i<$l; $i++) {
5048 next SMIX0 # it's a duplicate
5049 if($my_json_ref->[$i]->{'id_str'} == $id);
5050 if($my_json_ref->[$i]->{'id_str'} < $id) {
5051 $m = $i;
5052 last SMIX1; # got it
5053 }
5054 }
5055 if ($m == -1) { # didn't find
5056 push (@{ $my_json_ref }, $j);
5057 } elsif ($m == 0) { # degenerate case
5058 unshift (@{ $my_json_ref }, $j);
5059 } else { # did find, so splice
5060 splice(@{ $my_json_ref }, $m, 0,
5061 $j);
5062 }
5063 $l++;
5064 }
5065 }
5066 }
5067 %filter_next = ();
5068
5069 # fetch_id gyration. initially start with last_id, then roll. we
5070 # want to keep a window, though, so we try to pick a sensible value
5071 # that doesn't fetch too much but includes some overlap. we can't
5072 # do computations on the ID itself, because it's "opaque."
5073 $fetch_id = 0 if ($last_id == 0);
5074 &send_removereadline if ($termrl);
5075 if ($dont_refresh_first_time) {
5076 $last_id = &max($my_json_ref->[0]->{'id_str'}, $last_id);
5077 } else {
5078 ($last_id, $crap) =
5079 &tdisplay($my_json_ref, undef, $relative_last_id);
5080 }
5081 my $new_fi = (scalar(@{ $my_json_ref })) ?
5082 $my_json_ref->[(scalar(@{ $my_json_ref })-1)]->{'id_str'} :
5083 '';
5084 # try to widen the window to a "reasonable amount"
5085 $fetch_id = ($fetch_id == 0) ? $last_id :
5086 (length($new_fi) && $new_fi ne $last_id
5087 && $new_fi > $fetch_id) ? $new_fi :
5088 ($relative_last_id > 0 && $relative_last_id ne $last_id &&
5089 $relative_last_id > $fetch_id) ?
5090 $relative_last_id : $fetch_id;
5091
5092 print $stdout
5093 "-- last_id $last_id, fetch_id $fetch_id, rollback $relative_last_id\n".
5094 "-- (@{[ scalar(keys %id_cache) ]} cached)\n"
5095 if ($verbose);
5096 &send_removereadline if ($termrl);
5097 &$conclude;
5098 $wrapseq = 1;
5099 &send_repaint if ($termrl);
5100 }
5101
5102 # convenience function for filters (see below)
5103 sub killtw { my $j = shift; $filtered++; $filter_next{$j->{'id_str'}}++
5104 if ($is_background); }
5105
5106 # handle (i.e., display) an array of tweets in standard format
5107 sub tdisplay { # used by both synchronous /again and asynchronous refreshes
5108 my $my_json_ref = shift;
5109 my $class = shift;
5110 my $relative_last_id = shift;
5111 my $mini_id = shift;
5112 my $printed = 0;
5113 my $disp_max;
5114 my $save_counter = -1;
5115 my $i;
5116 my $j;
5117 my $return_j;
5118 my $t;
5119 my %ids;
5120 my $injected_json_ref = [];
5121
5122 # This is a little messy, but I can't think of a better way until I properly understand return values from tdisplay
5123 # Set return values based on original json structure
5124 # Note: Where does $last_id come from?
5125 $return_j = $my_json_ref->[0];
5126 $return_max = &max($my_json_ref->[0]->{'id_str'}, $last_id);
5127
5128 # Build hash of IDs passed to this subroutine
5129 foreach $t (@{ $my_json_ref }) {
5130 $ids{ $t->{'id_str'} } = 1;
5131 }
5132 # Inject quote tweets, but only if not already at parent level in $my_json_ref
5133 # This prevents /thread from displaying them twice
5134 # Twitter website only displays one level of quotation so no looping through, use /thread for more
5135 foreach $t (@{ $my_json_ref }) {
5136 $parent_t = $t;
5137 if ((length($t->{'quoted_status_id_str'})) || (length($t->{'retweeted_status'}->{'id_str'}))) {
5138 # If it is a retweet, get the original status and check that for quoted_status
5139 if (length($t->{'retweeted_status'}->{'id_str'})) {
5140 $t = $t->{'retweeted_status'};
5141 };
5142 $t = $t->{'quoted_status'};
5143 # Using smartmatch would be easier, but we are kind on older versions of Perl
5144 if (($t) && !exists($ids{$t->{'id_str'}})) {
5145 # Add reference to allow badging in standardtweet
5146 $t->{'oysttyer_quoted'} = 'true';
5147 push(@{ $injected_json_ref }, $t);
5148 }
5149 }
5150 # Push the parent after the quote to get ordering correct
5151 # Don't remove the url from the parent tweet though: https://twittercommunity.com/t/api-returns-url-to-twitters-status-update-at-the-end-of-the-text/50424/8
5152 push(@{ $injected_json_ref }, $parent_t);
5153 }
5154 $my_json_ref = $injected_json_ref;
5155 # Set display max to suit injected json
5156 $disp_max = &min($print_max, scalar(@{ $my_json_ref }));
5157
5158 if ($disp_max) { # null list may be valid if we get code 304
5159 unless ($is_background) { # reset store hash each console
5160 if ($mini_id) {
5161 # TODO:
5162 # generalize this at some point instead of hardcoded menu codes
5163 # maybe an ma0-mz9?
5164 $save_counter = $tweet_counter;
5165 $tweet_counter = $mini_split;
5166 for(0..9) {
5167 undef $store_hash{"zz$_"};
5168 }
5169 }# else {
5170 # $tweet_counter = $back_split;
5171 # %store_hash = ();
5172 #}
5173 }
5174 for($i = $disp_max; $i > 0; $i--) {
5175 my $g = ($i-1);
5176 $j = $my_json_ref->[$g];
5177 my $id = $j->{'id_str'};
5178 my $sn = $j->{'user'}->{'screen_name'};
5179 next if (!length($sn));
5180 $sn = lc(&descape($sn));
5181
5182 #
5183 # implement filter stages:
5184 # do so in such a way that we can toss tweets out
5185 # quickly, because multiple layers eat CPU!
5186 #
5187
5188 # zeroth: if this is us, do not filter.
5189 if (($anonymous || $sn ne $whoami) && !($nofilter)) {
5190
5191 # first, filterusers. this is very fast.
5192 # do for the tweet
5193 (&killtw($j), next) if
5194 ($filterusers_sub &&
5195 &$filterusers_sub($sn));
5196 # and if the tweet has a retweeted status, do for
5197 # that.
5198 (&killtw($j), next) if
5199 ($j->{'retweeted_status'} &&
5200 $filterusers_sub &&
5201 &$filterusers_sub(lc(&descape($j->
5202 {'retweeted_status'}->
5203 {'user'}->{'screen_name'}))));
5204
5205 # second, filterrts. this is almost as fast.
5206 (&killtw($j), next) if
5207 ($filterrts_sub &&
5208 (length($j->{'retweeted_status'}->{'id_str'}) || length($j->{'quoted_status_id_str'}))&&
5209 &$filterrts_sub($sn));
5210
5211 # third, filteratonly. this has a fast case and a
5212 # slow case.
5213 my $tex = &descape($j->{'text'});
5214 (&killtw($j), next) if
5215 ($filteratonly_sub &&
5216 &$filteratonly_sub($sn) && # fast test
5217 $tex !~ /\@$whoami\b/i); # slow test
5218
5219 # fourth, filterats. this is somewhat expensive.
5220 (&killtw($j), next) if ($filterats_c &&
5221 &$filterats_c($tex));
5222
5223 # finally, classic -filter. this is the most expensive.
5224 (&killtw($j), next) if ($filter_c && &$filter_c($tex));
5225 }
5226
5227 # damn it, user may actually want this tweet.
5228 # assign menu codes and place into caches
5229 $key = (($is_background) ? '' : 'z' ).
5230 substr($alphabet, $tweet_counter/10, 1) .
5231 $tweet_counter % 10;
5232 $tweet_counter =
5233 ($tweet_counter == 259) ? $mini_split :
5234 ($tweet_counter == ($mini_split - 1))
5235 ? 0 : ($tweet_counter+1);
5236 $j->{'menu_select'} = $key;
5237 $key = lc($key);
5238
5239 # recover ID cache memory: find the old ID with this
5240 # menu code and remove it, then add the new one
5241 # except if this is the foreground. we don't use this
5242 # in the foreground.
5243 if ($is_background) {
5244 delete $id_cache{$store_hash{$key}->{'id_str'}};
5245 $id_cache{$id} = $key;
5246 }
5247
5248 # finally store in menu code cache
5249 $store_hash{$key} = $j;
5250
5251 sleep 5 while ($suspend_output > 0);
5252 &send_removereadline if ($termrl);
5253 $wrapseq++;
5254
5255 $printed += scalar(&$handle($j,
5256 ($class || (($id <= $relative_last_id) ? 'again' :
5257 undef))));
5258 }
5259 }
5260 $tweet_counter = $save_counter if ($save_counter > -1);
5261 sleep 5 while ($suspend_output > 0);
5262 &$exception(6,"*** warning: more tweets than menu codes; truncated\n")
5263 if (scalar(@{ $my_json_ref }) > $print_max);
5264 if (($interactive || $verbose) && !$printed) {
5265 &send_removereadline if ($termrl);
5266 print $stdout "-- sorry, nothing to display.\n";
5267 $wrapseq = 1;
5268 }
5269 return ($return_max, $return_j);
5270 }
5271
5272 sub dt_tdisplay {
5273 my $my_json_ref = shift;
5274 my $class = shift;
5275 if (defined($my_json_ref)
5276 && ref($my_json_ref) eq 'ARRAY'
5277 && scalar(@{ $my_json_ref })) {
5278 my ($crap, $art) = &tdisplay($my_json_ref, $class);
5279 unless ($timestamp) {
5280 my ($time, $ts1) = &$wraptime(
5281 $my_json_ref->[(&min($print_max,scalar(@{ $my_json_ref }))-1)]->{'created_at'});
5282 my ($time, $ts2) = &$wraptime($art->{'created_at'});
5283 print $stdout &wwrap(
5284 "-- update covers $ts1 thru $ts2\n");
5285 }
5286 &$conclude;
5287 }
5288 }
5289
5290 # thump for DMs
5291 sub dmrefresh {
5292 my $interactive = shift;
5293 my $sent_dm = shift;
5294 # for streaming API to inject DMs it receives
5295 my $my_json_ref = shift;
5296
5297 if ($anonymous) {
5298 print $stdout
5299 "-- sorry, you can't read DMs if you're anonymous.\n"
5300 if ($interactive);
5301 return;
5302 }
5303
5304 # no point in doing this if we can't even get to our own timeline
5305 # (unless user specifically requested it, or our timeline is off)
5306 return if (!$interactive && !$last_id && !$notimeline); # NOT last_dm
5307
5308 $my_json_ref = &grabjson((($sent_dm) ? "$dmsenturl?full_text=true" : "$dmurl?full_text=true"),
5309 (($sent_dm) ? 0 : $last_dm), 0, $dmfetchwanted, undef, 1)
5310 if (!defined($my_json_ref) ||
5311 ref($my_json_ref) ne 'ARRAY');
5312 return if (!defined($my_json_ref)
5313 || ref($my_json_ref) ne 'ARRAY');
5314
5315 my $orig_last_dm = $last_dm;
5316 $last_dm = 0 if ($sent_dm);
5317
5318 $dmfetchwanted = 0;
5319 my $printed = 0;
5320 my $max = 0;
5321 my $disp_max = &min($print_max, scalar(@{ $my_json_ref }));
5322 my $i;
5323 my $g;
5324 my $key;
5325
5326 if ($disp_max) { # an empty list can be valid
5327 if ($dm_first_time) {
5328 sleep 5 while ($suspend_output > 0);
5329 &send_removereadline if ($termrl);
5330 print $stdout
5331 "-- checking for most recent direct messages:\n";
5332 $disp_max = 2;
5333 $interactive = 1;
5334 }
5335 for($i = $disp_max; $i > 0; $i--) {
5336 $g = ($i-1);
5337 my $j = $my_json_ref->[$g];
5338 next if (!$sent_dm && $j->{'id_str'} <= $last_dm);
5339 next if (!length($j->{'sender'}->{'screen_name'}) ||
5340 !length($j->{'recipient'}->{'screen_name'}));
5341
5342 $key = substr($alphabet, $dm_counter/10, 1) .
5343 $dm_counter % 10;
5344 $dm_counter =
5345 ($dm_counter == 259) ? 0 :
5346 ($dm_counter+1);
5347 $j->{'menu_select'} = $key;
5348 $dm_store_hash{lc($key)} = $j;
5349
5350 sleep 5 while ($suspend_output > 0);
5351 &send_removereadline if ($termrl);
5352 $wrapseq++;
5353
5354 $printed += scalar(&$dmhandle($j));
5355 }
5356 $max = $my_json_ref->[0]->{'id_str'};
5357 }
5358 sleep 5 while ($suspend_output > 0);
5359 if (($interactive || $verbose) && !$printed && !$dm_first_time) {
5360 &send_removereadline if ($termrl);
5361 print $stdout (($sent_dm)
5362 ? "-- you haven't sent anything yet.\n"
5363 : "-- sorry, no new direct messages.\n");
5364 $wrapseq = 1;
5365 }
5366 $last_dm = ($sent_dm) ? $orig_last_dm
5367 : &max($last_dm, $max);
5368 $dm_first_time = 0 if ($last_dm || !scalar(@{ $my_json_ref }));
5369 print $stdout "-- dm bookmark is $last_dm.\n" if ($verbose);
5370 &$dmconclude;
5371 &send_repaint if ($termrl);
5372 }
5373
5374 # post an update
5375 # this is a general API function that handles status updates and sending DMs.
5376 sub updatest {
5377 my $string = shift;
5378 my $interactive = shift;
5379 my $in_reply_to = shift;
5380 my $user_name_dm = shift;
5381 my $rt_id = shift; # even if this is set, string should also be set.
5382 my $urle = '';
5383 my $i;
5384 my $subpid;
5385 my $istring;
5386
5387 my $verb = (length($user_name_dm)) ? "DM $user_name_dm" :
5388 ($rt_id) ? 'RE-tweet' :
5389 'tweet';
5390
5391 if ($anonymous) {
5392 print $stdout
5393 "-- sorry, you can't $verb if you're anonymous.\n"
5394 if ($interactive);
5395 return 99;
5396 }
5397
5398 # "the pastebrake"
5399 if (!$slowpost && !$verify && !$script) {
5400 if ((time() - $postbreak_time) < 5) {
5401 $postbreak_count++;
5402 if ($postbreak_count == 3) {
5403 print $stdout
5404 "-- you're posting pretty fast. did you mean to do that?\n".
5405 "-- waiting three seconds before taking the next set of tweets\n".
5406 "-- hit CTRL-C NOW! to kill oysttyer if you accidentally pasted in this window\n";
5407 sleep 3;
5408 $postbreak_count = 0;
5409 }
5410 } else {
5411 $postbreak_count = 0;
5412 }
5413 $postbreak_time = time();
5414 }
5415
5416 my $payload = (length($user_name_dm)) ? 'text' : 'status';
5417 $string = &$prepost($string) unless ($user_name_dm || $rt_id);
5418
5419 # YES, you *can* verify and slowpost. I thought about this and I
5420 # think I want to allow it.
5421 if ($verify && !$status) {
5422 my $answer;
5423
5424 print $stdout
5425 &wwrap("-- verify you want to $verb: \"$string\"\n");
5426 $answer = lc(&linein(
5427 "-- send to server? (only y or Y is affirmative):"));
5428 if ($answer ne 'y') {
5429 print $stdout "-- ok, NOT sent to server.\n";
5430 return 97;
5431 }
5432 }
5433 unless ($rt_id) {
5434 $urle = '';
5435 #newlinehandler. New lines will indicated by "\n", which is two characters in the string
5436 #so need to keep track of backslashes that come up
5437 my $nlh = 'false';
5438 #To send a literal "\" followed by an "n" prefix with another "\". I.e. "\\n"
5439 #Therefore two "\\" should always work out to be just one "\"
5440 #TODO: Would be nice to remove some of the duplication below.
5441 foreach $i (unpack("${pack_magic}C*", $string)) {
5442 my $k = chr($i);
5443 if ($nlh eq 'true') {
5444 #Then already have a slash and need to check for an "n"
5445 if ($k eq "n") {
5446 #Encoding for a newline
5447 $urle .= "%0A";
5448 $nlh = 'false';
5449 } else {
5450 #There is no "n" so might need to send the slash we've been holding onto and next character
5451 if ($k ne "\\") {
5452 #If it isn't another slash send the slash we held onto
5453 $urle .= "%5C";
5454 }
5455 #Then send the character itself
5456 if ($k =~ /[-._~a-zA-Z0-9]/) {
5457 $urle .= $k;
5458 } else {
5459 $k = sprintf("%02X", $i);
5460 $urle .= "%$k";
5461 }
5462 #Clear handler
5463 $nlh = 'false';
5464 }
5465 } elsif ($k eq "\\") {
5466 #Could be the start of a new line
5467 $nlh = 'true';
5468 } else {
5469 #Handle how we've always handled it
5470 if ($k =~ /[-._~a-zA-Z0-9]/) {
5471 $urle .= $k;
5472 } else {
5473 $k = sprintf("%02X", $i);
5474 $urle .= "%$k";
5475 }
5476 #Clear handler
5477 $nlh = 'false';
5478 }
5479 }
5480 }
5481 if ($nlh eq 'true') {
5482 #Then last one was a slash
5483 $urle .= "%5C";
5484 }
5485
5486 $user_name_dm = (length($user_name_dm)) ?
5487 "&user=$user_name_dm" : '';
5488
5489 my $i = '';
5490 $i .= "source=oysttyer&" if ($authtype eq 'basic');
5491 $i .= "in_reply_to_status_id=${in_reply_to}&" if ($in_reply_to > 0);
5492 if (!$rt_id && defined $lat && defined $long && $location) {
5493 print $stdout "-- using lat/long: ($lat, $long)\n";
5494 $i .= "lat=${lat}&long=${long}&";
5495 } elsif ((defined $lat || defined $long) && $location && !$rt_id) {
5496 print $stdout
5497 "-- warning: incomplete location ($lat, $long) ignored\n";
5498 }
5499 $i .= "${payload}=${urle}${user_name_dm}" unless ($rt_id);
5500 $i .= "id=$rt_id" if ($rt_id);
5501 $slowpost += 0; if ($slowpost && !$script && !$status && !$silent) {
5502 if($pid = open(SLOWPOST, '-|')) {
5503 # pause background so that it doesn't kill itself
5504 # when this signal occurs.
5505 kill $SIGUSR1, $child;
5506 print $stdout &wwrap(
5507 "-- waiting $slowpost seconds to $verb, ^C cancels: \"$string\"\n");
5508 close(SLOWPOST); # this should wait for us
5509 if ($? > 256) {
5510 print $stdout
5511 "\n-- not sent, cancelled by user\n";
5512 return 97;
5513 }
5514 print $stdout "-- sending to server\n";
5515 kill $SIGUSR2, $child;
5516 &send_removereadline if ($termrl && $dostream);
5517 } else {
5518 $in_backticks = 1; # defeat END sub
5519 &sigify(sub { exit 254; }, qw(BREAK INT TERM PIPE));
5520 sleep $slowpost;
5521 exit 0;
5522 }
5523 }
5524 my $return = &backticks($baseagent, '/dev/null', undef,
5525 (length($user_name_dm)) ? $dmupdate :
5526 ($rt_id) ? "$rturl/${rt_id}.json" :
5527 $update, $i, 0, @wend);
5528 print $stdout "-- return --\n$return\n-- return --\n"
5529 if ($superverbose);
5530 if ($? > 0) {
5531 $x = $? >> 8;
5532 print $stdout <<"EOF" if ($interactive);
5533 ${MAGENTA}*** warning: connect timeout or no confirmation received ($x)
5534 *** to attempt a resend, type %%${OFF}
5535 EOF
5536 return $?;
5537 }
5538 my $ec;
5539 if ($ec = &is_json_error($return)) {
5540 print $stdout <<"EOF" if ($interactive);
5541 ${MAGENTA}*** warning: server error message received
5542 *** "$ec"${OFF}
5543 EOF
5544 return 98;
5545 }
5546 if ($ec = &is_fail_whale($return) ||
5547 $return =~ /^\[?\]?<!DOCTYPE\s+html/i ||
5548 $return =~ /^(Status:\s*)?50[0-9]\s/ ||
5549 $return =~ /^<html>/i ||
5550 $return =~ /^<\??xml\s+/) {
5551 print $stdout <<"EOF" if ($interactive);
5552 ${MAGENTA}*** warning: Twitter Fail Whale${OFF}
5553 EOF
5554 return 98;
5555 }
5556 $lastpostid = &parsejson($return)->{'id_str'};
5557 unless ($user_name_dm || $rt_id) {
5558 $lasttwit = $string;
5559 &$postpost($string);
5560 }
5561 return 0;
5562 }
5563
5564 # this dispatch routine replaces the common logic of deletest, deletedm,
5565 # follow, leave and the favourites system.
5566 # this is a modified, abridged version of &updatest.
5567 sub central_cd_dispatch {
5568 my ($payload, $interactive, $update) = (@_);
5569 my $return = &backticks($baseagent, '/dev/null', undef,
5570 $update, $payload, 0, @wend);
5571 print $stdout "-- return --\n$return\n-- return --\n"
5572 if ($superverbose);
5573 if ($? > 0) {
5574 $x = $? >> 8;
5575 print $stdout <<"EOF" if ($interactive);
5576 ${MAGENTA}*** warning: connect timeout or no confirmation received ($x)
5577 *** to attempt again, type %%${OFF}
5578 EOF
5579 return ($?, '');
5580 }
5581 my $ec;
5582 if ($ec = &is_json_error($return)) {
5583 print $stdout <<"EOF" if ($interactive);
5584 ${MAGENTA}*** warning: server error message received
5585 *** "$ec"${OFF}
5586 EOF
5587 return (98, $return);
5588 }
5589 return (0, $return);
5590 }
5591
5592 # the following functions may be user-exposed in a future version of
5593 # oysttyer, but are officially still "private interfaces."
5594 # delete a status
5595 sub deletest {
5596 my $id = shift;
5597 my $interactive = shift;
5598 my $url = $delurl;
5599
5600 $url =~ s/%I/$id/;
5601 my ($en, $em) = &central_cd_dispatch("id=$id", $interactive, $url);
5602 print $stdout "-- tweet id #${id} has been removed\n"
5603 if ($interactive && !$en);
5604 print $stdout "*** (was the tweet already deleted?)\n"
5605 if ($interactive && $en);
5606 return 0;
5607 }
5608
5609 # delete a DM
5610 sub deletedm {
5611 my $id = shift;
5612 my $interactive = shift;
5613
5614 my ($en, $em) = &central_cd_dispatch("id=$id", $interactive, $dmdelurl);
5615 print $stdout "-- DM id #${id} has been removed\n"
5616 if ($interactive && !$en);
5617 print $stdout "*** (was the DM already deleted?)\n"
5618 if ($interactive && $en);
5619 return 0;
5620 }
5621
5622 # create or destroy a favourite
5623 sub cordfav {
5624 my $id = shift;
5625 my $interactive = shift;
5626 my $basefav = shift;
5627 my $text = shift;
5628 my $verb = shift;
5629
5630 my ($en, $em) = &central_cd_dispatch("id=$id", $interactive, $basefav);
5631 print $stdout "-- like $verb for tweet id #${id}: \"$text\"\n"
5632 if ($interactive && !$en);
5633 print $stdout "*** (was the like already ${verb}?)\n"
5634 if ($interactive && $en);
5635 return 0;
5636 }
5637
5638 # follow or unfollow a user
5639 sub foruuser {
5640 my $uname = shift;
5641 my $interactive = shift;
5642 my $basef = shift;
5643 my $verb = shift;
5644
5645 my ($en, $em) = &central_cd_dispatch("screen_name=$uname",
5646 $interactive, $basef);
5647 print $stdout "-- ok, you have $verb following user $uname.\n"
5648 if ($interactive && !$en);
5649 return 0;
5650 }
5651
5652 # block or unblock a user
5653 sub boruuser {
5654 my $uname = shift;
5655 my $interactive = shift;
5656 my $basef = shift;
5657 my $verb = shift;
5658
5659 my ($en, $em) = &central_cd_dispatch("screen_name=$uname",
5660 $interactive, $basef);
5661 print $stdout "-- ok, you have $verb blocking user $uname.\n"
5662 if ($interactive && !$en);
5663 return 0;
5664 }
5665
5666 # mute or unmute a user
5667 sub muteuser {
5668 my $uname = shift;
5669 my $interactive = shift;
5670 my $basef = shift;
5671 my $verb = shift;
5672
5673 my ($en, $em) = &central_cd_dispatch("screen_name=$uname",
5674 $interactive, $basef);
5675 print $stdout "-- ok, you have $verb muting user $uname.\n"
5676 if ($interactive && !$en);
5677 return 0;
5678 }
5679
5680 # enable or disable retweets for a user
5681 sub rtsonoffuser {
5682 my $uname = shift;
5683 my $interactive = shift;
5684 my $selection = shift;
5685 my $verb = ($selection) ? 'enabled' : 'disabled';
5686 my $tval = ($selection) ? 'true' : 'false';
5687
5688 my ($en, $em) = &central_cd_dispatch(
5689 "retweets=${tval}&screen_name=${uname}",
5690 $interactive, $frupdurl);
5691 print $stdout "-- ok, you have ${verb} retweets for user $uname.\n"
5692 if ($interactive && !$en);
5693 return 0;
5694 }
5695
5696 #### oysttyer internal API utility functions ####
5697 # ... which your API *can* call
5698
5699 # gets and returns the contents of a URL (optionally pass a POST body)
5700 sub graburl {
5701 my $resource = shift;
5702 my $data = shift;
5703
5704 return &backticks($baseagent,
5705 '/dev/null', undef, $resource, $data,
5706 1, @wind);
5707 }
5708
5709 # format a tweet based on user options
5710 sub standardtweet {
5711 my $ref = shift;
5712 my $nocolour = shift;
5713
5714 my $sn = &descape($ref->{'user'}->{'screen_name'});
5715 my $tweet = &descape($ref->{'text'});
5716 my $colour;
5717 my $g;
5718 my $h;
5719 my $quote_badge = &descape("↑");
5720
5721 # wordwrap really ruins our day here, thanks a lot, @augmentedfourth
5722 # have to insinuate the ansi sequences after the string is wordwrapped
5723
5724 $g = $colour = ${'CC' . scalar(&$tweettype($ref, $sn, $tweet)) }
5725 unless ($nocolour);
5726 $colour = $OFF . $colour
5727 unless ($nocolour);
5728
5729 # prepend screen name "badges"
5730 $sn = "\@$sn" if ($ref->{'in_reply_to_status_id_str'} > 0);
5731 $sn = "+$sn" if ($ref->{'user'}->{'geo_enabled'} eq 'true' &&
5732 (($ref->{'geo'}->{'coordinates'}->[0] ne 'undef' &&
5733 length($ref->{'geo'}->{'coordinates'}->[0]) &&
5734 $ref->{'geo'}->{'coordinates'}->[1] ne 'undef' &&
5735 length($ref->{'geo'}->{'coordinates'}->[0])) ||
5736 length($ref->{'place'}->{'id'})));
5737 $sn = "%$sn" if (length($ref->{'retweeted_status'}->{'id_str'}));
5738 # badge the parent of the quoted tweet. Note: is_quote_status seems to be undocumented?
5739 $sn = "\"$sn" if ($ref->{'is_quote_status'} eq 'true');
5740 # badge quoted statuses themselves
5741 $sn = ($quote_badge . $sn) if ($ref->{'oysttyer_quoted'} eq 'true');
5742 $sn = "*$sn" if ($ref->{'source'} =~ /oysttyer/ && $oysttyeristas);
5743 # prepend list information, if this tweet originated from a list
5744 $sn = "($ref->{'tag'}->{'payload'})$sn"
5745 if (length($ref->{'tag'}->{'payload'}) &&
5746 $ref->{'tag'}->{'type'} eq 'list');
5747 $tweet = "<$sn> $tweet";
5748
5749 # twitter doesn't always do this right.
5750 $h = $ref->{'retweet_count'}; $h += 0; #$h = "${h}+" if ($h >= 100);
5751 # twitter doesn't always handle single retweets right. good f'n grief.
5752 $tweet = "(x${h}) $tweet" if ($h > 1 && !$nonewrts);
5753 # br3nda's modified timestamp patch
5754 if ($timestamp) {
5755 my ($time, $ts);
5756 # Print the timestamp of the original tweet, not when it was RTed.
5757 if (length($ref->{'retweeted_status'}->{'id_str'})) {
5758 ($time, $ts) = &wraptime($ref->{'retweeted_status'}->{'created_at'});
5759 } else {
5760 ($time, $ts) = &wraptime($ref->{'created_at'});
5761 }
5762 $tweet = "[$ts] $tweet";
5763 }
5764
5765 # pull it all together
5766 $tweet = &wwrap($tweet, ($wrapseq <= 1) ? ((&$prompt(1))[1]) : 0)
5767 if ($wrap); # remember to account for prompt length on #1
5768 $tweet =~ s/^([^<]*)<([^>]+)>/${g}\1<${EM}\2${colour}>/
5769 unless ($nocolour);
5770 $tweet =~ s/\n*$//;
5771 $tweet .= ($nocolour) ? "\n" : "$OFF\n";
5772
5773 # highlight anything that we have in track
5774 if(scalar(@tracktags)) { # I'm paranoid
5775 foreach $h (@tracktags) {
5776 $h =~ s/^"//; $h =~ s/"$//; # just in case
5777 $tweet =~ s/(^|[^a-zA-Z0-9])($h)([^a-zA-Z0-9]|$)/\1${EM}\2${colour}\3/ig
5778 unless ($nocolour);
5779 }
5780 }
5781
5782 # smb's underline/bold patch goes on last (modified for lists)
5783 unless ($nocolour) {
5784 # only do this after the < > portion.
5785 my $k = index($tweet, ">");
5786 my $botsub = substr($tweet, $k);
5787 my $topsub = substr($tweet, 0, $k);
5788 $botsub =~
5789 s/(^|[^a-zA-Z0-9_])\@([a-zA-Z0-9_\/]+)/\1\@${UNDER}\2${colour}/g;
5790 $tweet = $topsub . $botsub;
5791 }
5792
5793 if ($largeimages) {
5794 $tweet =~ s#(https://pbs.twimg.com/media/\S+)\.(png|jpg)#\1.\2\:large#g;
5795 } elsif ($origimages) {
5796 $tweet =~ s#(https://pbs.twimg.com/media/\S+)\.(png|jpg)#\1.\2\:orig#g;
5797 }
5798 return $tweet;
5799 }
5800
5801 # format a DM based on standard user options
5802 sub standarddm {
5803 my $ref = shift;
5804 my $nocolour = shift;
5805
5806 my ($time, $ts) = &$wraptime($ref->{'created_at'});
5807 my $text = &descape($ref->{'text'});
5808 my $sns = lc(&descape($ref->{'sender'}->{'screen_name'}));
5809 if ($sns eq $whoami) {
5810 $sns = "->" . &descape($ref->{'recipient'}->{'screen_name'});
5811 }
5812 my $g = &wwrap("[DM d$ref->{'menu_select'}]".
5813 "[$sns/$ts] $text", ($wrapseq <= 1) ? ((&$prompt(1))[1]) : 0);
5814
5815 $g =~ s/^\[DM ([^\/]+)\//${CCdm}[DM ${EM}\1${OFF}${CCdm}\//
5816 unless ($nocolour);
5817 $g =~ s/\n*$//;
5818 $g .= ($nocolour) ? "\n" : "$OFF\n";
5819 $g =~ s/(^|[^a-zA-Z0-9_])\@(\w+)/\1\@${UNDER}\2${OFF}${CCdm}/g
5820 unless ($nocolour);
5821 return $g;
5822 }
5823
5824 # format an event record based on standard user options (mostly for
5825 # streaming API, perhaps REST API one day)
5826 sub standardevent {
5827 my $ref = shift;
5828 my $nocolour = shift;
5829
5830 my $g = '>>> ';
5831 my $verb = &descape($ref->{'event'});
5832
5833 # https://dev.twitter.com/docs/streaming-apis/messages
5834
5835 if (length($verb)) { # see below for server-level events
5836 my $tar_sn = '@'.&descape($ref->{'target'}->{'screen_name'});
5837 my $sou_sn = '@'.&descape($ref->{'source'}->{'screen_name'});
5838
5839 my $tar_list_name = '';
5840 my $tar_list_desc = '';
5841
5842 # For all verbs starting with "list", get name and desc
5843 if ($verb =~ m/^list/ ) {
5844 $tar_list_name = &descape($ref->{'target_object'}->{'full_name'});
5845 $tar_list_desc = &descape($ref->{'target_object'}->{'description'});
5846 }
5847 # Twitter still uses (un)?favorite, but we add (un)?like as a bit of future-proofing.
5848 if ($verb eq 'like' || $verb eq 'unlike' || $verb eq 'favorite' || $verb eq 'unfavorite') {
5849 my $rto = &destroy_all_tco($ref->{'target_object'});
5850 my $txt = &descape($rto->{'text'});
5851 $verb =~ s/favorite/like/;
5852 $g .=
5853 "$sou_sn just ${verb}d ${tar_sn}'s tweet: \"$txt\"";
5854 } elsif ($verb eq 'liked_retweet' || $verb eq 'favorited_retweet') {
5855 # Put these in a separate case since the English gets a little more complicated
5856 my $rto = &destroy_all_tco($ref->{'target_object'});
5857 my $txt = &descape($rto->{'text'});
5858 $verb =~ s/favorite/like/;
5859 # This event gets sent for both likes of tweets you retweeted and when you
5860 # like a tweet that was retweeted into your timeline. We only want to
5861 # display this message in the former case.
5862 $g .= "$sou_sn just liked a tweet you retweeted: \"$txt\"" unless (lc($sou_sn) eq '@' . lc($whoami)) ;
5863 } elsif ($verb eq 'follow') {
5864 $g .= "$sou_sn is now following $tar_sn";
5865 } elsif ($verb eq 'user_update') {
5866 $g .= "$sou_sn updated their profile (/whois $sou_sn to see)";
5867 } elsif ($verb eq 'list_member_added') {
5868 $g .= "$sou_sn added $tar_sn to the list \"$tar_list_desc\" ($tar_list_name)";
5869 } elsif ($verb eq 'list_member_removed') {
5870 $g .= "$sou_sn removed $tar_sn from the list \"$tar_list_desc\" ($tar_list_name)";
5871 } elsif ($verb eq 'list_user_subscribed') {
5872 $g .= "$sou_sn is now following the list \"$tar_list_desc\" ($tar_list_name) from $tar_sn";
5873 } elsif ($verb eq 'list_user_unsubscribed') {
5874 $g .= "$sou_sn is no longer following the list \"$tar_list_desc\" ($tar_list_name) from $tar_sn";
5875 } elsif ($verb eq 'list_created') {
5876 $g .= "$sou_sn created the new list \"$tar_list_desc\" ($tar_list_name)";
5877 } elsif ($verb eq 'list_destroyed') {
5878 $g .= "$sou_sn destroyed the list \"$tar_list_desc\" ($tar_list_name)";
5879 } elsif ($verb eq 'list_updated') {
5880 $g .= "$sou_sn updated the list \"$tar_list_desc\" ($tar_list_name)";
5881 } elsif ($verb eq 'block' || $verb eq 'unblock') {
5882 $g .= "$sou_sn ${verb}ed $tar_sn ($tar_sn is not ".
5883 "notified)";
5884 } elsif ($verb eq 'mute' || $verb eq 'unmute') {
5885 $g .= "$sou_sn ${verb}d $tar_sn ($tar_sn is not ".
5886 "notified)";
5887 } elsif ($verb eq 'access_revoked') {
5888 $g .= "$sou_sn revoked oAuth access to $tar_sn";
5889 } elsif ($verb eq 'access_unrevoked') {
5890 $g .= "$sou_sn restored oAuth access to $tar_sn";
5891 } elsif ($verb eq 'quoted_tweet') {
5892 my $rto = &destroy_all_tco($ref->{'target_object'});
5893 my $txt = &descape($rto->{'text'});
5894 $g .= "$sou_sn just quoted ${tar_sn}'s tweet: \"$txt\"";
5895 } elsif ($verb eq 'retweeted_retweet') {
5896 my $rto = &destroy_all_tco($ref->{'target_object'});
5897 my $txt = &descape($rto->{'text'});
5898 $g .= "$sou_sn just retweeted a tweet you retweeted: \"$txt\"";
5899 } else {
5900 # try to handle new types of events we don't
5901 # recognize yet.
5902 $verb .= ($verb =~ /e$/) ? 'd' : 'ed';
5903 $g .= "$sou_sn $verb $tar_sn (basic)";
5904 }
5905
5906 # server events ("public stream messages") are handled differently.
5907 # we support almost all except for the ones that are irrelevant to
5908 # this medium.
5909
5910 } elsif ($ref->{'delete'}) {
5911 # this is the best we can do -- it's already on the screen!
5912 # we don't want to make it easy which tweet it is, since that
5913 # would be embarrassing, so just say a delete occurred.
5914 $g .=
5915 "tweet ID# ".$ref->{'delete'}->{'status'}->{'id_str'}.
5916 " deleted by server";
5917 } elsif ($ref->{'status_withheld'}) {
5918 # Twitter doesn't document id_str as available here. check.
5919 if (!length($ref->{'status_withheld'}->{'id_str'})) {
5920 # do nothing right now
5921 } else { $g .=
5922 "tweet ID# ".$ref->{'status_withheld'}->{'id_str'}.
5923 " censored by server in your country";
5924 }
5925 } elsif ($ref->{'user_withheld'}) {
5926 $g .=
5927 "user ID# ".$ref->{'user_withheld'}->{'user_id'}.
5928 " censored by server in your country";
5929 } elsif ($ref->{'disconnect'}) {
5930 $g .=
5931 "DISCONNECTED BY SERVER (".$ref->{'disconnect'}->{'code'}.
5932 "); will retry: ".$ref->{'disconnect'}->{'reason'};
5933 } else {
5934 # we have no idea what this is. just BS our way out.
5935 $g .= "unknown server event received (non-fatal)\n";
5936 }
5937
5938 if ($timestamp) {
5939 my ($time, $ts) = &$wraptime($ref->{'created_at'});
5940 $g = "[$ts] $g";
5941 }
5942
5943 $g = &wwrap("$g\n", ($wrapseq <= 1) ? ((&$prompt(1))[1]) : 0);
5944 # highlight screen names
5945 $g =~
5946 s/(^|[^a-zA-Z0-9_])\@([a-zA-Z0-9_\-\/]+)/\1\@${UNDER}\2${OFF}/g
5947 unless ($nocolour);
5948
5949 return $g;
5950 }
5951
5952 # for future expansion: this is the declared API callable method
5953 # for executing a command as if the console had typed it.
5954 sub ucommand {
5955 die("** can't call &ucommand during multi-module loading.\n")
5956 if ($multi_module_mode == -1);
5957 &prinput(@_);
5958 }
5959
5960 # your application can also call &grabjson to get a hashref
5961 # corresponding to parsed JSON from an arbitrary resource.
5962 # see that function later on.
5963
5964
5965 #### DEFAULT oysttyer INTERNAL API METHODS ####
5966 # don't change these here. instead, use -exts=yourlibrary.pl and set there.
5967 # note that these are all anonymous subroutine references.
5968 # anything you don't define is overwritten by the defaults.
5969 # it's better'n'superclasses.
5970 # NOTE: defaultaddaction, defaultmain and defaultprompt
5971 # are all defined in the "console" section above for
5972 # clarity.
5973
5974 # this first set are the multi-module aware ones.
5975
5976 # the standard iterator for multi-module methods
5977 sub multi_module_dispatch {
5978 my $default = shift;
5979 my $dispatch_chain = shift;
5980 my $rv_handler = shift;
5981 my @args = @_;
5982
5983 local $dispatch_ref; # on purpose; get_key/set_key may need it
5984 # $*_call_default is a global
5985 $did_call_default = 0;
5986 $this_call_default = 0;
5987 $multi_module_context = 0;
5988
5989 if ($rv_handler == 0) {
5990 $rv_handler = sub {
5991 return 0;
5992 };
5993 }
5994
5995 # fall through to default if no dispatch chain
5996 if (!scalar(@{ $dispatch_chain })) {
5997 return &$default(@args);
5998 }
5999 foreach $dispatch_ref (@{ $dispatch_chain }) {
6000 # each reference has the code, and the file that specified it.
6001 # set up a multi-module context and run that function. if the
6002 # default ever gets called, we log it to tell the multi-module
6003 # handler to call the default at the end.
6004
6005 my $rv;
6006 my $irv;
6007 my $caller = (caller(1))[3];
6008 $caller =~ s/^main::multi//;
6009
6010 $multi_module_context = 1; # defaults then know to defer
6011 $this_call_default = 0;
6012 $store = $master_store->{ $dispatch_ref->[0] };
6013 print "-- calling \$$caller in $dispatch_ref->[0]\n"
6014 if ($verbose);
6015 my $code_ref = $dispatch_ref->[1];
6016 $rv = &$rv_handler(@irv = &$code_ref(@args));
6017 $multi_module_context = 0;
6018 if ($rv & 4) {
6019 # rv_handler indicating to call default and halt
6020 # if it was called.
6021 return &$default(@args) if ($did_call_default);
6022 }
6023 if ($rv & 2) {
6024 # rv_handler indicating to make new @args from @irv
6025 @args = @irv;
6026 }
6027 if ($rv & 1) {
6028 # rv_handler indicating to halt early. do so.
6029 return (wantarray) ? @irv : $irv[0];
6030 }
6031 }
6032 $multi_module_context = 0;
6033 return &$default(@args) if ($did_call_default);
6034 return (wantarray) ? @irv : $irv[0];
6035 }
6036
6037 # these are the stubs that call the dispatcher.
6038 sub multiaddaction {
6039 &multi_module_dispatch(\&defaultaddaction, \@m_addaction, sub{
6040 # return immediately on the first extension to accept
6041 return (shift>0);
6042 }, @_);
6043 }
6044 sub multiconclude {
6045 &multi_module_dispatch(\&defaultconclude, \@m_conclude, 0, @_);
6046 }
6047 sub multidmconclude {
6048 &multi_module_dispatch(\&defaultdmconclude, \@m_dmconclude, 0, @_);
6049 }
6050 sub multidmhandle {
6051 &multi_module_dispatch(\&defaultdmhandle, \@m_dmhandle, sub {
6052 my $rv = shift;
6053
6054 # skip default calls.
6055 return 0 if ($this_call_default);
6056
6057 # if not a default call, and the DM was refused for
6058 # processing by this extension, then the DM is now
6059 # suppressed. do not call any other extensions after this.
6060 # even if it ends in suppression, we still call the default
6061 # if it was ever called before.
6062 return 5 if ($rv == 0);
6063
6064 # if accepted in any manner, keep calling.
6065 return 0;
6066 }, @_);
6067 }
6068 sub multieventhandle {
6069 &multi_module_dispatch(\&defaulteventhandle, \@m_eventhandle, sub {
6070 my $rv = shift;
6071
6072 # skip default calls.
6073 return 0 if ($this_call_default);
6074
6075 # if not a default call, and the event was refused for
6076 # processing by this extension, then the event is now
6077 # suppressed. do not call any other extensions after this.
6078 # even if it ends in suppression, we still call the default
6079 # if it was ever called before.
6080 return 5 if ($rv == 0);
6081
6082 # if accepted in any manner, keep calling.
6083 return 0;
6084 }, @_);
6085 }
6086 sub multiexception {
6087 # this is a secret option for people who want to suppress errors.
6088 if ($exception_is_maskable) {
6089 &multi_module_dispatch(\&defaultexception, \@m_exception, sub {
6090 my $rv = shift;
6091
6092 # same logic as handle/dmhandle, except return -1-
6093 # to mask from subsequent extensions.
6094 return 0 if ($this_call_default);
6095 return 5 if ($rv);
6096 return 0;
6097 }, @_);
6098 } else {
6099 &multi_module_dispatch(
6100 \&defaultexception, \@m_exception, 0, @_);
6101 }
6102 }
6103 sub multishutdown {
6104 return if ($shutdown_already_called++);
6105 &multi_module_dispatch(\&defaultshutdown, \@m_shutdown, 0, @_);
6106 }
6107
6108 sub multiuserhandle {
6109 &multi_module_dispatch(\&defaultuserhandle, \@m_userhandle, sub{
6110 # skip default calls.
6111 return 0 if ($this_call_default);
6112
6113 # return immediately on the first extension to accept
6114 return (shift>0);
6115 }, @_);
6116 }
6117 sub multilisthandle {
6118 &multi_module_dispatch(\&defaultlisthandle, \@m_listhandle, sub{
6119 # skip default calls.
6120 return 0 if ($this_call_default);
6121
6122 # return immediately on the first extension to accept
6123 return (shift>0);
6124 }, @_);
6125 }
6126 sub multihandle {
6127 &multi_module_dispatch(\&defaulthandle, \@m_handle, sub {
6128 my $rv = shift;
6129
6130 # skip default calls.
6131 return 0 if ($this_call_default);
6132
6133 # if not a default call, and the tweet was refused for
6134 # processing by this extension, then the tweet is now
6135 # suppressed. do not call any other extensions after this.
6136 # even if it ends in suppression, we still call the default
6137 # if it was ever called before.
6138 return 5 if ($rv==0);
6139
6140 # if accepted in any manner, keep calling.
6141 return 0;
6142 }, @_);
6143 }
6144 sub multiheartbeat {
6145 &multi_module_dispatch(\&defaultheartbeat, \@m_heartbeat, 0, @_);
6146 }
6147 sub multiprecommand {
6148 &multi_module_dispatch(\&defaultprecommand, \@m_precommand, sub {
6149 return 2; # feed subsequent chains the result.
6150 }, @_);
6151 }
6152 sub multiprepost {
6153 &multi_module_dispatch(\&defaultprepost, \@m_prepost, sub {
6154 return 2; # feed subsequent chains the result.
6155 }, @_);
6156 }
6157 sub multipostpost {
6158 &multi_module_dispatch(\&defaultpostpost, \@m_postpost, 0, @_);
6159 }
6160 sub multitweettype {
6161 &multi_module_dispatch(\&defaulttweettype, \@m_tweettype, sub {
6162 # if this module DID NOT call default, exit now.
6163 return (!$this_call_default);
6164 }, @_);
6165 }
6166
6167 sub flag_default_call { $this_call_default++; $did_call_default++; }
6168
6169 # now the actual default methods
6170
6171 sub defaultexception {
6172 (&flag_default_call, return) if ($multi_module_context);
6173 my $msg_code = shift;
6174 return if ($msg_code == 2 && $muffle_server_messages);
6175 my $message = "@_";
6176 $message =~ s/\n*$//sg;
6177 if ($timestamp) {
6178 my ($time, $ts) = &$wraptime(scalar(localtime));
6179 $message = "[$ts] $message";
6180 $message =~ s/\n/\n[$ts] /sg;
6181 }
6182 &send_removereadline if ($termrl);
6183 $wrapseq = 1;
6184 print $stdout "${MAGENTA}${message}${OFF}\n";
6185 &send_repaint if ($termrl);
6186 $laststatus = 1;
6187 }
6188 sub defaultshutdown {
6189 (&flag_default_call, return) if ($multi_module_context);
6190 }
6191 sub defaultlisthandle {
6192 (&flag_default_call, return) if ($multi_module_context);
6193 my $list_ref = shift;
6194
6195 print $streamout "*** for future expansion ***\n";
6196
6197 return 1;
6198 }
6199 sub defaulthandle {
6200 (&flag_default_call, return) if ($multi_module_context);
6201 my $tweet_ref = shift;
6202 my $class = shift;
6203 my $dclass = ($verbose) ? "{$class,$tweet_ref->{'id_str'}} " : '';
6204 my $sn = &descape($tweet_ref->{'user'}->{'screen_name'});
6205 my $tweet = &descape($tweet_ref->{'text'});
6206 my $stweet = &standardtweet($tweet_ref);
6207 my $menu_select = $tweet_ref->{'menu_select'};
6208
6209 $menu_select = (length($menu_select) && !$script)
6210 ? (($menu_select =~ /^z/) ?
6211 "${EM}${menu_select}>${OFF} " :
6212 "${menu_select}> ")
6213 : '';
6214 print $streamout "\n" if ($doublespace);
6215 print $streamout $menu_select . $dclass . $stweet;
6216 &sendnotifies($tweet_ref, $class);
6217 return 1;
6218 }
6219 sub defaultuserhandle {
6220 (&flag_default_call, return) if ($multi_module_context);
6221
6222 my $user_ref = shift;
6223 &userline($user_ref, $streamout);
6224 my $desc = &strim(&descape($user_ref->{'description'}));
6225 my $klen = ($wrap || 79) - 9;
6226 $klen = 10 if ($klen < 0);
6227 $desc = substr($desc, 0, $klen)."..." if (length($desc) > $klen);
6228 print $streamout (' "' . $desc . '"' . "\n") if (length($desc));
6229 return 1;
6230 }
6231 sub userline { # used by both $userhandle and /whois
6232 my $my_json_ref = shift;
6233 my $fh = shift;
6234
6235 my $verified =
6236 ($my_json_ref->{'verified'} eq 'true') ?
6237 "${EM}(Verified)${OFF} " : '';
6238 my $protected =
6239 ($my_json_ref->{'protected'} eq 'true') ?
6240 "${EM}(Protected)${OFF} " : '';
6241 print $fh <<"EOF";
6242 ${CCprompt}@{[ &descape($my_json_ref->{'name'}) ]}${OFF} (@{[ &descape($my_json_ref->{'screen_name'}) ]}) (f:$my_json_ref->{'friends_count'}/$my_json_ref->{'followers_count'}) (u:$my_json_ref->{'statuses_count'}) ${verified}${protected}
6243 EOF
6244 return;
6245 }
6246 sub sendnotifies { # this is a default subroutine of a sort, right?
6247 my $tweet_ref = shift;
6248 my $class = shift;
6249
6250 my $sn = &descape($tweet_ref->{'user'}->{'screen_name'});
6251 my $tweet = &descape($tweet_ref->{'text'});
6252
6253 # interactive? first time?
6254 unless (length($class) || !$last_id || !length($tweet)) {
6255 $class = scalar(&$tweettype($tweet_ref, $sn, $tweet));
6256 &notifytype_dispatch($class,
6257 &standardtweet($tweet_ref, 1), $tweet_ref)
6258 if ($notify_list{$class});
6259 }
6260 }
6261
6262 sub defaulttweettype {
6263 (&flag_default_call, return) if ($multi_module_context);
6264 my $ref = shift;
6265 my $sn = shift;
6266 my $tweet = shift;
6267
6268 # br3nda's and smb's modified colour patch
6269 unless ($anonymous) {
6270 if (lc($sn) eq $whoami) {
6271 # if it's me speaking, colour the line yellow
6272 return 'me';
6273 } elsif ($tweet =~ /\@$whoami(\b|$)/i) {
6274 # if I'm in the tweet, colour red
6275 return 'reply';
6276 }
6277 }
6278 if ($ref->{'class'} eq 'search') { # anonymous allows this too
6279 # if this is a search result, colour cyan
6280 return 'search';
6281 }
6282 if ($ref->{'tag'}->{'type'} eq 'list') { # anonymous allows this too
6283 return 'list';
6284 }
6285 return 'default';
6286 }
6287
6288 sub defaultconclude {
6289 (&flag_default_call, return) if ($multi_module_context);
6290 if ($filtered && $filter_attribs{'count'}) {
6291 print $stdout "-- (filtered $filtered tweets)\n";
6292 $filtered = 0;
6293 }
6294 }
6295
6296 sub defaultdmhandle {
6297 (&flag_default_call, return) if ($multi_module_context);
6298 my $dm_ref = shift;
6299 my $sns = &descape($dm_ref->{'sender'}->{'screen_name'});
6300
6301 print $streamout &standarddm($dm_ref);
6302 &senddmnotifies($dm_ref) if ($sns ne $whoami);
6303 return 1;
6304 }
6305
6306 sub senddmnotifies {
6307 my $dm_ref = shift;
6308 &notifytype_dispatch('DM', &standarddm($dm_ref, 1), $dm_ref)
6309 if ($notify_list{'dm'} && $last_dm);
6310 }
6311
6312 sub defaulteventhandle {
6313 (&flag_default_call, return) if ($multi_module_context);
6314 my $event_ref = shift;
6315 # in this version, we silently filter delete events, but your
6316 # extension would still get them delivered.
6317 return 1 if ($event_ref->{'delete'});
6318 print $streamout &standardevent($event_ref);
6319 return 1;
6320 }
6321
6322 sub defaultdmconclude {
6323 (&flag_default_call, return) if ($multi_module_context);
6324 }
6325
6326 sub defaultheartbeat {
6327 (&flag_default_call, return) if ($multi_module_context);
6328 }
6329
6330 # not much sense to multi-module protect these.
6331 sub defaultprecommand { return ("@_"); }
6332 sub defaultprepost { return ("@_"); }
6333
6334 sub defaultpostpost {
6335 (&flag_default_call, return) if ($multi_module_context);
6336 my $line = shift;
6337 return if (!$termrl);
6338
6339 # populate %readline_completion if readline is on
6340 while($line =~ s/^\@(\w+)\s+//) {
6341 $readline_completion{'@'.lc($1)}++;
6342 }
6343 if ($line =~ /^[dD]\s+(\w+)\s+/) {
6344 $readline_completion{'@'.lc($1)}++;
6345 }
6346 }
6347
6348 sub defaultautocompletion {
6349 my ($text, $line, $start) = (@_);
6350 my $qmtext = quotemeta($text);
6351 my @proband;
6352 my @rlkeys;
6353
6354 # handle / completion
6355 if ($start == 0 && $text =~ m#^/#) {
6356 return sort grep(/^$qmtext/i, '/history',
6357 '/print', '/quit', '/bye', '/again',
6358 '/wagain', '/whois', '/thump', '/dm', '/qdm',
6359 '/refresh', '/dmagain', '/set', '/help',
6360 '/reply', '/url', '/thread', '/retweet', '/replyall',
6361 '/replies', '/ruler', '/exit', '/me', '/vcheck',
6362 '/oretweet', '/eretweet', '/lretweet', '/liston',
6363 '/listoff', '/dmsent', '/rtsof', '/rtson', '/rtsoff',
6364 '/lists', '/withlist', '/add', '/padd', '/push',
6365 '/pop', '/followers', '/friends', '/lfollow',
6366 '/lleave', '/listfollowers', '/listfriends',
6367 '/unset', '/verbose', '/short', '/follow', '/unfollow',
6368 '/doesfollow', '/search', '/tron', '/troff',
6369 '/delete', '/deletelast', '/dump',
6370 '/track', '/trends', '/block', '/unblock',
6371 '/mute', '/unmute', '/web',
6372 '/like', '/likes', '/unlike', '/eval');
6373 }
6374 @rlkeys = keys(%readline_completion);
6375
6376 # handle @ completion. this works slightly weird because
6377 # readline hands us the string WITHOUT the @, so we have to
6378 # test somewhat blindly. this works even if a future readline
6379 # DOES give us the word with @. also handles D, /wa, /wagain,
6380 # /a, /again, etc.
6381 if (($line =~ m#^(D|/wa|/wagain|/a|/again) #i) ||
6382 ($start == 1 && substr($line, 0, 1) eq '@') ||
6383 # this code is needed to prevent inline @ from flipping out
6384 ($start >= 1 && substr($line, ($start-2), 2) eq ' @')) {
6385 @proband = grep(/^\@$qmtext/i, @rlkeys);
6386 if (scalar(@proband)) {
6387 @proband = map { s/^\@//;$_ } @proband;
6388 return @proband;
6389 }
6390 }
6391 # definites that are left over, including @ if it were included
6392 if(scalar(@proband = grep(/^$qmtext/i, @rlkeys))) {
6393 return @proband;
6394 }
6395
6396 # heuristics
6397 # URL completion (this doesn't always work of course)
6398 if ($text =~ m#https?://#) {
6399 return (&urlshorten($text) || $text);
6400 }
6401
6402 # "I got nothing."
6403 return ();
6404 }
6405
6406 #### built-in notification routines ####
6407
6408 # growl for Mac OS X
6409 sub notifier_growl {
6410 my $class = shift;
6411 my $text = shift;
6412 my $ref = shift; # not used in this version
6413
6414 if (!defined($class) || !length($notify_tool_path)) {
6415 # we are being asked to initialize
6416 $notify_tool_path = &wherecheck("trying to find growlnotify",
6417 "growlnotify",
6418 "growlnotify must be installed to use growl notifications. check your\n" .
6419 "documentation for how to do this.\n")
6420 unless ($notify_tool_path);
6421 if (!defined($class)) {
6422 return 1 if ($script || $notifyquiet);
6423 $class = 'Growl support activated';
6424 $text =
6425 'You can configure notifications for oysttyer in the Growl preference pane.';
6426 }
6427 }
6428 # handle this in the background for faster performance.
6429 # to avoid problems with SIGCHLD, we fork ourselves twice (mmm!),
6430 # leaving an orphan which init should grab (we need SIGCHLD for
6431 # proper backticks, so it can't be IGNOREd).
6432 my $gchild;
6433 if ($gchild = fork()) {
6434 # the parent harvests the child, which will die immediately.
6435 waitpid($gchild, 0);
6436 return 1;
6437 } elsif (!defined ($gchild)) {
6438 print $stdout "warning: failed growl fork: $!\n";
6439 return 1;
6440 }
6441 # this is the child. spawn, then exit and abandon our own child,
6442 # which init will reap. the problem with teen pregnancy is mounting.
6443 $in_backticks = 1;
6444 my $hchild;
6445 if ($hchild = fork()) {
6446 exit;
6447 } elsif (!defined ($hchild)) {
6448 print $stdout "warning: failed growl fork: $!\n";
6449 exit;
6450 }
6451 # this is the subchild, which is abandoned at a fire sta^W^W^Winit.
6452 open(GROWL, "|$notify_tool_path -n 'oysttyer' 'oysttyer: $class'");
6453 binmode(GROWL, ":utf8") unless ($seven);
6454 print GROWL $text;
6455 close(GROWL);
6456 exit;
6457 }
6458
6459 # libnotify for {Linux,whatevs}
6460 # this is EXPERIMENTAL, and requires this patch to notify-send:
6461 # http://www.floodgap.com/software/ttytter/libnotifypatch.txt
6462 # why it has not already been applied is fricking beyond me, it makes
6463 # sense. would YOU want arbitrary characters on the command line
6464 # separated only from overwriting your home directory by a quoting routine?
6465 sub notifier_libnotify {
6466 my $class = shift;
6467 my $text = shift;
6468 my $ref = shift; # not used in this version
6469
6470 if (!defined($class) || !defined($notify_tool_path)) {
6471 # we are being asked to initialize
6472 $notify_tool_path = &wherecheck("trying to find notify-send",
6473 "notify-send",
6474 "notify-send must be installed to use libnotify, and it must be modified\n".
6475 "for standard input. see the documentation for how to do this.\n")
6476 unless ($notify_tool_path);
6477 if (!defined($class)) {
6478 return 1 if ($script || $notifyquiet);
6479 $class = 'libnotify support activated';
6480 $text =
6481 'Congratulations, your notify-send is correctly configured for oysttyer.';
6482 }
6483 }
6484 # figure out the time to display based on length of tweet
6485 my $t = 1000+50*length($text); # about 150-180wpm read speed
6486 open(NOTIFYSEND,
6487 "|$notify_tool_path -t $t -f - 'oysttyer: $class'");
6488 binmode(NOTIFYSEND, ":utf8") unless ($seven);
6489 print NOTIFYSEND $text;
6490 close(NOTIFYSEND);
6491 return 1;
6492 }
6493
6494 #### IPC routines for communicating between the foreground + background ####
6495
6496 # this is the central routine that takes a rolling tweet code, figures
6497 # out where that tweet is, and returns something approximating a tweet
6498 # structure (or the actual tweet structure itself if it can).
6499 sub get_tweet {
6500 my $code = lc(shift);
6501
6502 #TODO
6503 # implement querying the id_cache here. we need IPC for it, though.
6504 # if the code is all numbers, treat it like an id_str, and try
6505 # to get it from the server. we have similar code in get_dm.
6506 # the first tweet that is of relevance is ID 20. try /dump 20 :)
6507 return &grabjson("${idurl}?id=${code}", 0, 0, 0, undef, 1)
6508 if ($code =~ /^[0-9]+$/ && (0+$code > 19));
6509
6510 return undef if ($code !~ /^z?[a-z][0-9]$/);
6511 my $source = ($code =~ /^z/) ? 1 : 0;
6512 my $k = '';
6513 my $l = '';
6514 my $w = {'user' => {}};
6515
6516 if ($is_background) {
6517 if ($source == 1) { # foreground only
6518 return undef;
6519 }
6520 return $store_hash{$code};
6521 }
6522 return $store_hash{$code} if ($source); # foreground c/foreground twt
6523
6524 print $stdout "-- querying background: $code\n" if ($verbose);
6525 kill $SIGUSR2, $child if ($child);
6526 print C "pipet $code ----------\n";
6527 while(length($k) < $packet_length) {
6528 sysread(W, $l, $packet_length);
6529 $k .= $l;
6530 }
6531 return undef if ($k !~ /[^\s]/);
6532 $k =~ s/\s+$//; # remove trailing spaces
6533 print $stdout "-- background store fetch: $k\n" if ($verbose);
6534 # And I think any additional field extracted from the stream also has to be added here as well.
6535 # I.e quoted_status_id_str
6536 # Need to increment the count in split at the end.
6537 ($w->{'menu_select'}, $w->{'id_str'}, $w->{'in_reply_to_status_id_str'},
6538 $w->{'quoted_status_id_str'},
6539 $w->{'quoted_status'}->{'text'},
6540 $w->{'quoted_status'}->{'full_text'},
6541 $w->{'quoted_status'}->{'extended_tweet'}->{'full_text'},
6542 $w->{'retweeted_status'}->{'id_str'},
6543 $w->{'retweeted_status'}->{'text'},
6544 $w->{'retweeted_status'}->{'full_text'},
6545 $w->{'retweeted_status'}->{'extended_tweet'}->{'full_text'},
6546 $w->{'retweeted_status'}->{'quoted_status'}->{'id_str'},
6547 $w->{'retweeted_status'}->{'quoted_status'}->{'text'},
6548 $w->{'retweeted_status'}->{'quoted_status'}->{'full_text'},
6549 $w->{'retweeted_status'}->{'quoted_status'}->{'extended_tweet'}->{'full_text'},
6550 $w->{'user'}->{'geo_enabled'},
6551 $w->{'geo'}->{'coordinates'}->[0],
6552 $w->{'geo'}->{'coordinates'}->[1],
6553 $w->{'place'}->{'id'},
6554 $w->{'place'}->{'country_code'},
6555 $w->{'place'}->{'place_type'},
6556 $w->{'place'}->{'full_name'},
6557 $w->{'tag'}->{'type'},
6558 $w->{'tag'}->{'payload'},
6559 $w->{'retweet_count'},
6560 $w->{'user'}->{'screen_name'}, $w->{'created_at'},
6561 $w->{'source'}, $k) = split(/\0/, $k, 29);
6562 $w->{'text'} = pack("H*", $k);
6563 $w->{'place'}->{'full_name'} = pack("H*",$w->{'place'}->{'full_name'});
6564 $w->{'tag'}->{'payload'} = pack("H*", $w->{'tag'}->{'payload'});
6565 return undef if (!length($w->{'text'})); # unpossible
6566 $w->{'created_at'} =~ s/_/ /g;
6567 return $w;
6568 }
6569
6570 # this is the analogous function for a rolling DM code. it is somewhat
6571 # simpler as DM codes are always rolling and have no foreground store
6572 # currently, so it always executes a background request.
6573 sub get_dm {
6574 my $code = lc(shift);
6575 my $k = '';
6576 my $l = '';
6577 my $w = {'sender' => {}};
6578 my $t1 = '';
6579 my $t2 = '';
6580 return undef if (length($code) < 3 || $code !~ s/^d//);
6581
6582 # this is the aforementioned "similar code" (see get_tweet).
6583 # optimization: I doubt ANY of us can get DMIDs less than 9.
6584 return &grabjson("${dmidurl}?id=$code", 0, 0, 0, undef, 1)
6585 if ($code =~ /^[0-9]+$/ && (0+$code > 9));
6586
6587 return undef if ($code !~ /^[a-z][0-9]$/);
6588
6589 kill $SIGUSR2, $child if ($child); # prime pipe
6590 print C "piped $code ----------\n"; # internally two alphanum, recall
6591 while(length($k) < $packet_length) {
6592 sysread(W, $l, $packet_length);
6593 $k .= $l;
6594 }
6595
6596 return undef if ($k !~ /[^\s]/);
6597 $k =~ s/\s+$//; # remove trailing spaces
6598 print $stdout "-- background store fetch: $k\n" if ($verbose);
6599 ($w->{'menu_select'}, $w->{'id_str'},
6600 $w->{'sender'}->{'screen_name'}, $w->{'created_at'},
6601 $l) = split(/\s/, $k, 5);
6602 #Truncate text if a little bit too long
6603 ($t1, $t2) = &csplit(pack("H*", $l), "word");
6604 if (length($t2)) {
6605 $t1 .= "...";
6606 }
6607 $w->{'text'} = $t1;
6608 return undef if (!length($w->{'text'})); # not possible
6609 $w->{'created_at'} =~ s/_/ /g;
6610 return $w;
6611 }
6612
6613 # this function requests a $store key from the background. it only works
6614 # if foreground.
6615 sub getbackgroundkey {
6616 if ($is_background) {
6617 print $stdout "*** can't call getbackgroundkey from background\n";
6618 return undef;
6619 }
6620 my $key = shift;
6621 my $l;
6622 my $k;
6623 print C substr("ki $key ---------------------", 0, 19)."\n";
6624 my $ref = (length($dispatch_ref->[0])) ? ($dispatch_ref->[0]) :
6625 "DEFAULT";
6626 print C substr(unpack("${pack_magic}H*", $ref).$space_pad, 0, $packet_length);
6627 while(length($k) < $packet_length) {
6628 sysread(W, $l, $packet_length);
6629 $k .= $l;
6630 }
6631 $k =~ s/[^0-9a-fA-F]//g;
6632 print $stdout "-- background store fetch: $k\n" if ($verbose);
6633 return pack("H*", $k);
6634 }
6635
6636 # this function sends a $store key to the background. it only works if
6637 # foreground.
6638 sub sendbackgroundkey {
6639 if ($is_background) {
6640 print $stdout "*** can't call sendbackgroundkey from background\n";
6641 return;
6642 }
6643 my $key = shift;
6644 my $value = shift;
6645 if (ref($value)) {
6646 print $stdout "*** send_key only supported for scalars\n";
6647 return;
6648 }
6649 if (!length($value)) {
6650 print C substr("kn $key ---------------------", 0, 19)."\n";
6651 } else {
6652 print C substr("ko $key ---------------------", 0, 19)."\n";
6653 }
6654 my $ref = (length($dispatch_ref->[0])) ? ($dispatch_ref->[0]) :
6655 "DEFAULT";
6656 print C substr(unpack("${pack_magic}H*", $ref).$space_pad, 0, $packet_length);
6657 return if (!length($value));
6658 print C substr(unpack("${pack_magic}H*", $value).$space_pad, 0, $packet_length);
6659 }
6660
6661 # hold stolen from Floodgap's Texapp
6662 sub hold {
6663 $holdhold ^= 1;
6664 print C "hold---------------\n" unless ($synch);
6665 &sync_semaphore;
6666 }
6667
6668 sub thump { print C "update-------------\n"; &sync_semaphore; }
6669 sub dmthump { print C "dmthump------------\n"; &sync_semaphore; }
6670
6671 # ensure_held and ensure_not_held stolen from Floodgap's Texapp
6672 sub ensure_held {
6673 return if ($holdhold || $synch);
6674 &hold;
6675 }
6676 sub ensure_not_held {
6677 return if (!$holdhold || $synch);
6678 &hold;
6679 }
6680
6681 sub sync_n_quit {
6682 if ($child) {
6683 print $stdout "waiting for child ...\n" unless ($silent);
6684 print C "sync---------------\n";
6685 waitpid $child, 0;
6686 $child = 0;
6687 print $stdout "exiting.\n" unless ($silent);
6688 exit ($? >> 8);
6689 }
6690 exit;
6691 }
6692
6693 # setter for internal variables, with all the needed side effects for those
6694 # variables that are programmed to trigger internal actions when changed.
6695 sub setvariable {
6696 my $key = shift;
6697 my $value = shift;
6698 my $interactive = 0+shift;
6699
6700 $value =~ s/^\s+//;
6701 $value =~ s/\s+$//; # mostly to avoid problems with /(p)add
6702
6703 if ($key eq 'script') { # this can never be changed by this routine
6704 print $stdout "*** script may only be changed on init\n";
6705 return 1;
6706 }
6707 if ($key eq 'tquery' && $value eq '0') { # undo tqueries
6708 $tquery = undef;
6709 $key = 'track';
6710 $value = $track; # falls thru to sync
6711 &tracktags_makearray;
6712 }
6713 if ($opts_can_set{$key} ||
6714 # we CAN set read-only variables during initialization
6715 ($multi_module_mode == -1 && $valid{$key})) {
6716 if (length($value) > 1023) {
6717 # can't transmit this in a packet
6718 print $stdout "*** value too long\n";
6719 return 1;
6720 } elsif ($opts_boolean{$key} && $value ne '0' &&
6721 $value ne '1') {
6722 print $stdout "*** 0|1 only (boolean): $key\n";
6723 return 1;
6724 } elsif ($opts_urls{$key} &&
6725 $value !~ m#^(http|https|gopher)://#) {
6726 print $stdout "*** must be valid URL: $key\n";
6727 return 1;
6728 } else {
6729 KEYAGAIN: $$key = $value;
6730 print $stdout "*** changed: $key => $$key\n"
6731 if ($interactive || $verbose);
6732
6733 # handle special values
6734 &generate_ansi if ($key eq 'ansi' ||
6735 $key =~ /^colour/);
6736 &generate_shortdomain if ($key eq 'shorturl');
6737 &tracktags_makearray if ($key eq 'track');
6738 &filter_compile if ($key eq 'filter');
6739 &notify_compile if ($key eq 'notifies');
6740 &list_compile if ($key eq 'lists');
6741 &filterflags_compile if ($key eq 'filterflags');
6742 $filterrts_sub = &filteruserlist_compile(
6743 $filterrts_sub, $value)
6744 if ($key eq 'filterrts');
6745 $filterusers_sub = &filteruserlist_compile(
6746 $filterusers_sub,$value)
6747 if ($key eq 'filterusers');
6748 $filteratonly_sub = &filteruserlist_compile(
6749 $filteratonly_sub, $value)
6750 if ($key eq 'filteratonly');
6751 &filterats_compile if ($key eq 'filterats');
6752
6753 # transmit to background process sync-ed values
6754 if ($opts_sync{$key}) {
6755 &synckey($key, $value, $interactive);
6756 }
6757 if ($key eq 'superverbose') {
6758 if ($value eq '0') {
6759 $key = 'verbose';
6760 $value = $supreturnto;
6761 goto KEYAGAIN;
6762 }
6763 $supreturnto = $verbose;
6764 }
6765 # parse showusername
6766 if ($key eq 'showusername') {
6767 if ($value eq '1') {
6768 $showusername = 1;
6769 }
6770 }
6771 }
6772 # virtual keys
6773 } elsif ($key eq 'tquery') {
6774 my $ivalue = &tracktags_tqueryurlify($value);
6775 if (length($ivalue) > 139) {
6776 print $stdout
6777 "*** custom query is too long (encoded: $ivalue)\n";
6778 return 1;
6779 } else {
6780 $tquery = $value;
6781 &synckey($key, $ivalue, $interactive);
6782 }
6783 } elsif ($valid{$key}) {
6784 print $stdout
6785 "*** read-only, must change on command line: $key\n";
6786 return 1;
6787 } else {
6788 print $stdout
6789 "*** not a valid option or setting: $key\n";
6790 return 1;
6791 }
6792 return 0;
6793 }
6794 sub synckey {
6795 my $key = shift;
6796 my $value = shift;
6797 my $interactive = 0+shift;
6798 my $commchar = ($interactive) ? '=' : '+';
6799 print $stdout "*** (transmitting to background)\n"
6800 if ($interactive || $verbose);
6801 return if (!$child);
6802 kill $SIGUSR2, $child if ($child);
6803 print C
6804 (substr("${commchar}$key ", 0, 19) . "\n");
6805 print C (substr(($value . $space_pad), 0, $packet_length));
6806 sleep 1;
6807 }
6808
6809 # getter for internal variables. right now this just returns the variable by
6810 # name and a couple virtuals, but in the future this might be expanded.
6811 sub getvariable {
6812 my $key = shift;
6813 if ($valid{$key}) {
6814 return $$key;
6815 }
6816 if ($key eq 'effpause' ||
6817 $key eq 'rate_limit_rate' ||
6818 $key eq 'rate_limit_left') {
6819 my $value;
6820 kill $SIGUSR2, $child if ($child);
6821 print C (substr("?$key ", 0, 19) . "\n");
6822 sysread(W, $value, $packet_length);
6823 $value =~ s/\s+$//;
6824 return $value;
6825 }
6826 return undef;
6827 }
6828
6829 # compatibility stub for extensions calling the old wraptime
6830 sub wraptime { return &$wraptime(@_); }
6831
6832 #### url management (/url, /short) ####
6833
6834 sub generate_shortdomain {
6835 my $x;
6836 my $y;
6837
6838 undef $shorturldomain;
6839 ($shorturl =~ m#^http://([^/]+)/#) && ($x = $1);
6840 # chop off any leading hostname stuff (like api., etc.)
6841 while(1) {
6842 $y = $x;
6843 $x =~ s/^[^\.]*\.//;
6844 if ($x !~ /\./) { # a cut too far
6845 $shorturldomain = "http://$y/";
6846 last;
6847 }
6848 }
6849 print $stdout "-- warning: couldn't parse shortener service\n"
6850 if (!length($shorturldomain));
6851 }
6852
6853 sub openurl {
6854 my $comm = $urlopen;
6855 my $url = shift;
6856 $url = "http://gopher.floodgap.com/gopher/gw?".&url_oauth_sub($url)
6857 if ($url =~ m#^gopher://# && $comm !~ /^[^\s]*lynx/);
6858 $urlshort = $url;
6859 $comm =~ s/\%U/'$url'/g;
6860 print $stdout "($comm)\n";
6861 system("$comm");
6862 }
6863
6864 sub urlshorten {
6865 my $url = shift;
6866 my $rc;
6867 my $cl;
6868
6869 $url = "http://gopher.floodgap.com/gopher/gw?".&url_oauth_sub($url)
6870 if ($url =~ m#^gopher://#);
6871 return $url if ($url =~ /^$shorturldomain/i); # stop loops
6872 $url = &url_oauth_sub($url);
6873 $cl = "$simple_agent \"${shorturl}$url\"";
6874 print $stdout "$cl\n" if ($superverbose);
6875 chomp($rc = `$cl`);
6876 if ($rc =~ m#^https?://#) {
6877 return $rc
6878 } else {
6879 print $stdout "ERROR: " . "$rc\n";
6880 return undef
6881 }
6882 }
6883
6884 ##### optimizers -- these compile into an internal format #####
6885
6886 # utility routine for tquery support
6887 sub tracktags_tqueryurlify {
6888 my $value = shift;
6889 $value =~ s/([^ a-z0-9A-Z_])/"%".unpack("H2",$1)/eg;
6890 $value =~ s/\s/+/g;
6891 $value = "q=$value" if ($value !~ /^q=/);
6892 return $value;
6893 }
6894
6895 # tracking subroutines
6896 # run when a string is passed
6897 sub tracktags_makearray {
6898 @tracktags = ();
6899 $track =~ s/^'//; $track =~ s/'$//; $track = lc($track);
6900 if (!length($track)) {
6901 @trackstrings = ();
6902 return;
6903 }
6904 my $k;
6905 my $l = '';
6906 my $q = 0;
6907 my %w;
6908 my (@ptags) = split(/\s+/, $track);
6909
6910 # filter duplicates and merge quoted strings
6911 foreach $k (@ptags) {
6912 if ($q && $k =~ /"$/) { # this has to be first
6913 $l .= " $k";
6914 $q = 0;
6915 } elsif ($k =~ /^"/ || $q) {
6916 $l .= (length($l)) ? " $k" : $k;
6917 $q = 1;
6918 next;
6919 } else {
6920 $l = $k;
6921 }
6922
6923 if ($w{$l}) {
6924 print $stdout
6925 "-- warning: dropping duplicate track term \"$l\"\n";
6926 } elsif (uc($l) eq 'OR' || uc($l) eq 'AND') {
6927 print $stdout
6928 "-- warning: dropping unnecessary logical op \"$l\"\n";
6929 } else {
6930 $w{$l} = 1;
6931 push(@tracktags, $l);
6932 }
6933 $l = '';
6934 }
6935 print $stdout "-- warning: syntax error, missing quote?\n" if ($q);
6936 $track = join(' ', @tracktags);
6937 &tracktags_compile;
6938 }
6939 # run when array is altered (based on @kellyterryjones' code)
6940 sub tracktags_compile {
6941 @trackstrings = ();
6942 return if (!scalar(@tracktags));
6943
6944 my $k;
6945 my $l = '';
6946 # need to limit track tags to a certain number of pieces
6947 TAGBAG: foreach $k (@tracktags) {
6948 if (length($k) > 130) { # I mean, really
6949 print $stdout
6950 "-- warning: track tag \"$k\" is TOO LONG\n";
6951 next TAGBAG;
6952 }
6953 if (length($l)+length($k) > 150) { # balance of size/querytime
6954 push(@trackstrings, "q=".&url_oauth_sub($l));
6955 $l = '';
6956 }
6957 $l = (length($l)) ? "${l} OR ${k}" : "${k}";
6958 }
6959 push(@trackstrings, "q=".&url_oauth_sub($l)) if (length($l));
6960 }
6961
6962 # notification multidispatch
6963 sub notifytype_dispatch {
6964 return if (!scalar(@notifytypes));
6965 my $nt; foreach $nt (@notifytypes) { &$nt(@_); }
6966 }
6967
6968 # notifications compiler
6969 sub notify_compile {
6970 if ($notifies) {
6971 my $w;
6972
6973 undef %notify_list;
6974 foreach $w (split(/\s*,\s*/, $notifies)) {
6975 $notify_list{$w} = 1;
6976 }
6977 $notifies = join(',', keys %notify_list);
6978 }
6979 }
6980
6981 # lists compiler
6982 # we don't check the validity of lists here; /liston and /listoff do that.
6983 sub list_compile {
6984 my @oldlistlist = @listlist;
6985 my %already;
6986
6987 undef @listlist;
6988 if ($lists) {
6989 my $w;
6990 my $u;
6991 my $l;
6992 foreach $w (split(/\s*,\s*/, $lists)) {
6993 $w =~ s/^@//;
6994 if ($w =~ m#/#) {
6995 ($u, $l) = split(m#\s*/\s*#, $w, 2);
6996 } else {
6997 $l = $w;
6998 }
6999 if (!length($u) && $anonymous) {
7000 print $stdout "*** must use fully specified lists when anonymous\n";
7001 @listlist = @oldlistlist;
7002 return 0;
7003 }
7004 $u ||= $whoami;
7005 if ($l =~ m#/#) {
7006 print $stdout "*** syntax error in list $u/$l\n";
7007 @listlist = @oldlistlist;
7008 return 0;
7009 }
7010 if ($already{"$u/$l"}++) {
7011 print $stdout "*** duplicate list $u/$l ignored\n";
7012 } else {
7013 push(@listlist, [ $u, $l ]);
7014 }
7015 }
7016 $lists = join(',', keys %already);
7017 }
7018 return 1;
7019 }
7020
7021 # -filterflags compiler (replaces old -filter syntax)
7022 sub filterflags_compile {
7023 my $s = $filterflags;
7024 undef %filter_attribs;
7025 $s =~ s/^\s*['"]?\s*//;
7026 $s =~ s/\s*['"]?\s*$//;
7027 return if (!length($s));
7028 %filter_attribs = map { $_ => 1 } split(/\s*,\s*/, $s);
7029 }
7030
7031 # -filterrts and -filterusers compiler. these simply use a list of usernames,
7032 # so they are fast and the same code suffices. emit code to compile that
7033 # just is one if-expression after another.
7034 sub filteruserlist_compile {
7035 my $old = shift;
7036 my $s = shift;
7037 undef $k;
7038 $s =~ s/^\s*['"]?\s*//;
7039 $s =~ s/\s*['"]?\s*$//;
7040 return $k if (!length($s));
7041 my @us = map { $k=lc($_); "\$sn eq '$k'" } split(/\s*,\s*/, $s);
7042 my $uus = join(' || ', @us);
7043 my $uuus = <<"EOF";
7044 \$k = sub {
7045 my \$sn = shift;
7046 return 1 if ($uus);
7047 return 0;
7048 };
7049 EOF
7050 # print $stdout $uuus;
7051 eval $uuus;
7052 if (!defined($k)) {
7053 print $stdout "** bogus name in user list (error = $@)\n";
7054 return $old;
7055 }
7056 return $k;
7057 }
7058
7059 # -filterats compiler. this takes a list of usernames and then compiles a
7060 # whole bunch of regexes.
7061 sub filterats_compile {
7062 undef $filterats_c;
7063 my $s = $filterats;
7064 $s =~ s/^\s*['"]?\s*//;
7065 $s =~ s/\s*['"]?\s*$//;
7066 return 1 if (!length($s)); # undef
7067 my @us = map { $k=lc($_); "\$x=~/\\\@$k\\b/i" } split(/\s*,\s*/, $s);
7068 my $uus = join(' || ', @us);
7069 my $uuus = <<"EOF";
7070 \$filterats_c = sub {
7071 my \$x = shift;
7072 return 1 if ($uus);
7073 return 0;
7074 };
7075 EOF
7076 # print $stdout $uuus;
7077 eval $uuus;
7078 if (!defined($filterats_c)) {
7079 print $stdout "** bogus name in user list (error = $@)\n";
7080 return 0;
7081 }
7082 return 1;
7083 }
7084
7085 # -filter compiler. this is the generic case.
7086 sub filter_compile {
7087 undef %filter_attribs unless (length($filterflags));
7088 undef $filter_c;
7089 if (length($filter)) {
7090 my $tfilter = $filter;
7091 $tfilter =~ s/^['"]//;
7092 $tfilter =~ s/['"]$//;
7093 # note attributes (compatibility)
7094 while ($tfilter =~ s/^([a-z]+),//) {
7095 my $atkey = $1;
7096 $filter_attribs{$atkey}++;
7097 print $stdout
7098 "** $atkey filter parameter should be in -filterflags\n";
7099 }
7100 my $b = <<"EOF";
7101 \$filter_c = sub {
7102 local \$_ = shift;
7103 return ($tfilter);
7104 };
7105 EOF
7106 #print $b;
7107 eval $b;
7108 if (!defined($filter_c)) {
7109 print $stdout ("** syntax error in your filter: $@\n");
7110 return 0;
7111 }
7112 }
7113 return 1;
7114 }
7115
7116 #### common system subroutines follow ####
7117
7118 sub updatecheck {
7119 my $vcheck_url =
7120 "https://raw.githubusercontent.com/oysttyer/oysttyer/master/version_check.txt";
7121 my $vrlcheck_url =
7122 "http://www.floodgap.com/software/ttytter/01readlin.txt";
7123 my $update_url = shift;
7124
7125 my $vs = '';
7126 my $vvs;
7127 my $tverify;
7128 my $inversion;
7129 my $bversion;
7130 my $rcnum;
7131 my $download;
7132 my $maj;
7133 my $min;
7134 my $s1, $s2, $s3;
7135 my $update_trlt = undef;
7136
7137 if ($termrl && $termrl->ReadLine eq 'Term::ReadLine::TTYtter') {
7138 my $trlv = $termrl->Version;
7139 print $stdout
7140 "-- checking Term::ReadLine::TTYtter version: $vrlcheck_url\n";
7141 $vvs = `$simple_agent $vrlcheck_url`;
7142 print $stdout "-- server response: $vvs\n" if ($verbose);
7143 ($vvs, $s1, $s2, $s3) = split(/--__--\n/s, $vvs);
7144 $s1 = undef if ($s1 !~ /^\*/) ;
7145 $s2 = undef if ($s2 !~ /^\*/) ;
7146 $s3 = undef if ($s3 !~ /^\*/) ;
7147 chomp($vvs);
7148 # right now we're only using $inversion (no betas/rcs).
7149 ($tverify, $inversion, $bversion, $rcnum, $download,
7150 $bdownload) = split(/;/, $vvs, 6);
7151 if ($tverify ne 'trlt') {
7152 $vs .= "-- warning: unable to verify Term::ReadLine::TTYtter version\n";
7153 } else {
7154 if ($trlv < 0+$inversion) {
7155 $vs .= "** NEW Term::ReadLine::TTYtter VERSION AVAILABLE: $inversion **\n" .
7156 "** GET IT: $download\n";
7157 $update_trlt = $download;
7158 } else {
7159 $vs .= "-- your version of Term::ReadLine::TTYtter is up to date ($trlv)\n";
7160 }
7161 }
7162 }
7163
7164 print $stdout "-- checking oysttyer version: $vcheck_url\n";
7165 $vvs = `$simple_agent $vcheck_url`;
7166 print $stdout "-- server response: $vvs\n" if ($verbose);
7167 ($vvs, $s1, $s2, $s3) = split(/--__--\n/s, $vvs);
7168 $s1 = undef if ($s1 !~ /^\*/) ;
7169 $s2 = undef if ($s2 !~ /^\*/) ;
7170 $s3 = undef if ($s3 !~ /^\*/) ;
7171 chomp($vvs);
7172 ($tverify, $inversion, $bversion, $rcnum, $download, $bdownload) =
7173 split(/;/, $vvs, 6);
7174 if ($tverify ne 'oysttyer') {
7175 $vs .= "-- warning: unable to verify oysttyer version\n";
7176 } else {
7177 if ($my_version_string eq $bversion) {
7178 $vs .=
7179 "** REMINDER: you are using a beta version (${my_version_string}b${oysttyer_RC_NUMBER})\n";
7180 $vs .=
7181 "** NEW oysttyer RELEASE CANDIDATE AVAILABLE: build $rcnum **\n" .
7182 "** get it: $bdownload\n$s2"
7183 if ($oysttyer_RC_NUMBER < $rcnum);
7184 $vs .= "** (this is the most current beta)\n"
7185 if ($oysttyer_RC_NUMBER == $rcnum);
7186 $vs .= "$s1$s3";
7187 if ($oysttyer_RC_NUMBER < $rcnum) {
7188 if ($update_url) {
7189 $vs .=
7190 "-- %URL% is now $bdownload (/short shortens, /url opens)\n";
7191 $urlshort = $bdownload;
7192 }
7193 } elsif (length($update_trlt) && $update_url) {
7194 $urlshort = $update_trlt;
7195 $vs .= "-- %URL% is now $urlshort (/short shortens, /url opens)\n";
7196 }
7197 return $vs;
7198 }
7199 if ($my_version_string eq $inversion && $oysttyer_RC_NUMBER) {
7200 $vs .=
7201 "** FINAL oysttyer RELEASE NOW AVAILABLE for version $inversion **\n" .
7202 "** get it: $download\n$s2$s1";
7203 if ($update_url) {
7204 $vs .=
7205 "-- %URL% is now $bdownload (/short shortens, /url opens)\n";
7206 $urlshort = $bdownload;
7207 }
7208 return $vs;
7209 }
7210 ($inversion =~/^(\d+\.\d+)\.(\d+)$/) && ($maj = 0+$1,
7211 $min = 0+$2);
7212 if (0+$oysttyer_VERSION < $maj ||
7213 (0+$oysttyer_VERSION == $maj &&
7214 $oysttyer_PATCH_VERSION < $min)) {
7215 $vs .=
7216 "** NEWER oysttyer VERSION NOW AVAILABLE: $inversion **\n" .
7217 "** get it: $download\n$s2$s1";
7218 if ($update_url) {
7219 $vs .=
7220 "-- %URL% is now $download (/short shortens, /url opens)\n";
7221 $urlshort = $download;
7222 }
7223 return $vs;
7224 } elsif (0+$oysttyer_VERSION > $maj ||
7225 (0+$oysttyer_VERSION == $maj &&
7226 $oysttyer_PATCH_VERSION > $min)) {
7227 $vs .=
7228 "** unable to identify your version of oysttyer\n$s1";
7229 } else {
7230 $vs .=
7231 "-- your version of oysttyer is up to date ($inversion)\n$s1";
7232 }
7233 }
7234
7235 # if we got this far, then there is no oysttyer update, but maybe a
7236 # T:RL:T update, so we offer that as the URL
7237 if (length($update_trlt) && $update_url) {
7238 $urlshort = $update_trlt;
7239 $vs .= "-- %URL% is now $urlshort (/short shortens, /url opens)\n";
7240 }
7241 return $vs;
7242
7243 $vs .= "-- your version of oysttyer is ($my_version_string)\n";
7244 return $vs;
7245 }
7246
7247 sub generate_otabcomp {
7248 if (scalar(@j = keys(%readline_completion))) {
7249 # print optimized readline. include all that we
7250 # manually specified, plus/including top @s, total 10.
7251 @keys = sort { $readline_completion{$b} <=>
7252 $readline_completion{$a} } @j;
7253 $factor = $readline_completion{$keys[0]};
7254 foreach(keys %original_readline) {
7255 $readline_completion{$_} += $factor;
7256 }
7257 print $stdout "*** optimized readline:\n";
7258 @keys = sort { $readline_completion{$b} <=>
7259 $readline_completion{$a} } keys
7260 %readline_completion;
7261 @keys = @keys[0..14] if (scalar(@keys) > 15);
7262 print $stdout "-readline=\"@keys\"\n";
7263 }
7264 }
7265 sub end_me { exit; } # which falls through to, via END, ...
7266 sub killkid {
7267 # for streaming assistance
7268 if ($child) {
7269 print $stdout "\n\ncleaning up.\n";
7270 kill $SIGHUP, $child; # warn it about shutdown
7271 if (length($track)) {
7272 print $stdout "*** you were tracking:\n";
7273 print $stdout "-track='$track'\n";
7274 }
7275 if (length($filter)) {
7276 print $stdout "*** your current filter expression:\n";
7277 print $stdout "-filter='$filter'\n";
7278 }
7279 &generate_otabcomp;
7280 sleep 2 if ($dostream);
7281 kill 9, $curlpid if ($curlpid);
7282 kill 9, $child;
7283 }
7284 &$shutdown unless (!$shutdown);
7285 }
7286
7287 sub rmlock {
7288
7289 return unless (($lockf) && (-f $lockf));
7290 return unless (open(L, "<$lockf"));
7291
7292 while (<L>) {
7293 chomp();
7294 next unless (/^\d+$/);
7295 if ($_ == $$) {
7296 unlink($lockf);
7297 last;
7298 }
7299 }
7300
7301 close(L);
7302 }
7303
7304 sub generate_ansi {
7305 my $k;
7306
7307 $BLUE = ($ansi) ? "${ESC}[34;1m" : '';
7308 $RED = ($ansi) ? "${ESC}[31;1m" : '';
7309 $GREEN = ($ansi) ? "${ESC}[32;1m" : '';
7310 $YELLOW = ($ansi) ? "${ESC}[33m" : '';
7311 $MAGENTA = ($ansi) ? "${ESC}[35m" : '';
7312 $CYAN = ($ansi) ? "${ESC}[36m" : '';
7313
7314 $EM = ($ansi) ? "${ESC}[1m" : '';
7315 $UNDER = ($ansi) ? "${ESC}[4m" : '';
7316 $OFF = ($ansi) ? "${ESC}[0m" : '';
7317
7318 foreach $k (qw(prompt me dm reply warn search list default)) {
7319 ${"colour$k"} = uc(${"colour$k"});
7320 if (!defined($${"colour$k"})) {
7321 print $stdout
7322 "-- warning: bogus colour '".${"colour$k"}."'\n";
7323 } else {
7324 eval("\$CC$k = \$".${"colour$k"});
7325 }
7326 }
7327
7328 eval '$termrl->hook_use_ansi' if ($termrl);
7329 }
7330
7331 # always POST
7332 sub postjson {
7333 my $url = shift;
7334 my $postdata = shift; # add _method=DELETE for delete
7335 my $data;
7336
7337 # this is copied mostly verbatim from grabjson
7338 chomp($data = &backticks($baseagent, '/dev/null', undef, $url,
7339 $postdata, 0, @wend));
7340 my $k = $? >> 8;
7341
7342 $data =~ s/[\r\l\n\s]*$//s;
7343 $data =~ s/^[\r\l\n\s]*//s;
7344
7345 if (!length($data) || $k == 28 || $k == 7 || $k == 35) {
7346 &$exception(1, "*** warning: timeout or no data\n");
7347 return undef;
7348 }
7349
7350 # old non-JSON based error reporting code still supported
7351 if ($data =~ /^\[?\]?<!DOCTYPE\s+html/i || $data =~ /^(Status:\s*)?50[0-9]\s/ || $data =~ /^<html>/i || $data =~ /^<\??xml\s+/) {
7352 print $stdout $data if ($superverbose);
7353 if (&is_fail_whale($data)) {
7354 &$exception(2, "*** warning: Twitter Fail Whale\n");
7355 } else {
7356 &$exception(2, "*** warning: Twitter error message received\n" .
7357 (($data =~ /<title>Twitter:\s*([^<]+)</) ?
7358 "*** \"$1\"\n" : ''));
7359 }
7360 return undef;
7361 }
7362 if ($data =~ /^rate\s*limit/i) {
7363 print $stdout $data if ($superverbose);
7364 &$exception(3,
7365 "*** warning: exceeded API rate limit for this interval.\n" .
7366 "*** no updates available until interval ends.\n");
7367 return undef;
7368 }
7369
7370 if ($k > 0) {
7371 &$exception(4,
7372 "*** warning: unexpected error code ($k) from user agent\n");
7373 return undef;
7374 }
7375
7376 # handle things like 304, or other things that look like HTTP
7377 # error codes
7378 if ($data =~ m#^HTTP/\d\.\d\s+(\d+)\s+#) {
7379 $code = 0+$1;
7380 print $stdout $data if ($superverbose);
7381
7382 # 304 is actually a cop-out code and is not usually
7383 # returned, so we should consider it a non-fatal error
7384 if ($code == 304 || $code == 200 || $code == 204) {
7385 &$exception(1, "*** warning: timeout or no data\n");
7386 return undef;
7387 }
7388 &$exception(4,
7389 "*** warning: unexpected HTTP return code $code from server\n");
7390 return undef;
7391 }
7392
7393 # test for error/warning conditions with trivial case
7394 if ($data =~ /^\s*\{\s*(['"])(warning|error)\1\s*:\s*\1([^\1]*?)\1/s
7395 || $data =~ /(['"])(warning|error)\1\s*:\s*\1([^\1]*?)\1\}/s) {
7396 print $stdout $data if ($superverbose);
7397 &$exception(2, "*** warning: server $2 message received\n" .
7398 "*** \"$3\"\n");
7399 return undef;
7400 }
7401
7402 return &parsejson($data);
7403 }
7404
7405 # always GET
7406 sub grabjson {
7407 my $data;
7408 my $url = shift;
7409 my $last_id = shift;
7410 my $is_anon = shift;
7411 my $count = shift;
7412 my $tag = shift;
7413 my $do_entities = shift;
7414
7415 my $kludge_search_api_adjust = 0;
7416 my $my_json_ref = undef; # durrr hat go on foot
7417 my $i;
7418 my $tdata;
7419 my $seed;
7420
7421 #undef $/; $data = <STDIN>;
7422
7423 # we may need to sort our args for more flexibility here.
7424 my @xargs = (); my $i = index($url, "?");
7425 if ($i > -1) {
7426 # throw an error if "?" is at the end.
7427 push(@xargs, split(/\&/, substr($url, ($i+1))));
7428 $url = substr($url, 0, $i);
7429 }
7430
7431 # Use the extended mode that doesn't count URLs, etc in the character count
7432 push(@xargs, "tweet_mode=extended") if ($extended);
7433
7434 # count needs to be removed for the default case due to show, etc.
7435 push(@xargs, "count=$count") if ($count);
7436 # timeline control. this speeds up parsing since there's less data.
7437 # can't use skip_user: no SN
7438 push (@xargs, "since_id=${last_id}") if ($last_id);
7439
7440 # request entities, which should be supported everywhere now
7441 push (@xargs, "include_entities=1") if ($do_entities);
7442
7443 my $resource = (scalar(@xargs)) ?
7444 [ $url, join('&', sort @xargs) ] : $url;
7445
7446 chomp($data = &backticks($baseagent,
7447 '/dev/null', undef, $resource, undef,
7448 $is_anon + $anonymous, @wind));
7449 my $k = $? >> 8;
7450
7451 $data =~ s/[\r\l\n\s]*$//s;
7452 $data =~ s/^[\r\l\n\s]*//s;
7453
7454 if (!length($data) || $k == 28 || $k == 7 || $k == 35) {
7455 &$exception(1, "*** warning: timeout or no data\n");
7456 return undef;
7457 }
7458
7459 # old non-JSON based error reporting code still supported
7460 if ($data =~ /^\[?\]?<!DOCTYPE\s+html/i || $data =~ /^(Status:\s*)?50[0-9]\s/ || $data =~ /^<html>/i || $data =~ /^<\??xml\s+/) {
7461 print $stdout $data if ($superverbose);
7462 if (&is_fail_whale($data)) {
7463 &$exception(2, "*** warning: Twitter Fail Whale\n");
7464 } else {
7465 &$exception(2, "*** warning: Twitter error message received\n" .
7466 (($data =~ /<title>Twitter:\s*([^<]+)</) ?
7467 "*** \"$1\"\n" : ''));
7468 }
7469 return undef;
7470 }
7471 if ($data =~ /^rate\s*limit/i) {
7472 print $stdout $data if ($superverbose);
7473 &$exception(3,
7474 "*** warning: exceeded API rate limit for this interval.\n" .
7475 "*** no updates available until interval ends.\n");
7476 return undef;
7477 }
7478
7479 if ($k > 0) {
7480 &$exception(4,
7481 "*** warning: unexpected error code ($k) from user agent\n");
7482 return undef;
7483 }
7484
7485 # handle things like 304, or other things that look like HTTP
7486 # error codes
7487 if ($data =~ m#^HTTP/\d\.\d\s+(\d+)\s+#) {
7488 $code = 0+$1;
7489 print $stdout $data if ($superverbose);
7490
7491 # 304 is actually a cop-out code and is not usually
7492 # returned, so we should consider it a non-fatal error
7493 if ($code == 304 || $code == 200 || $code == 204) {
7494 &$exception(1, "*** warning: timeout or no data\n");
7495 return undef;
7496 }
7497 &$exception(4,
7498 "*** warning: unexpected HTTP return code $code from server\n");
7499 return undef;
7500 }
7501
7502 # test for error/warning conditions with trivial case
7503 if ($data =~ /^\s*\{\s*(['"])(warning|error)\1\s*:\s*\1([^\1]*?)\1/s
7504 || $data =~ /(['"])(warning|error)\1\s*:\s*\1([^\1]*?)\1\}/s) {
7505 print $stdout $data if ($superverbose);
7506 &$exception(2, "*** warning: server $2 message received\n" .
7507 "*** \"$3\"\n");
7508 return undef;
7509 }
7510
7511 # if wrapped in statuses object, unwrap it
7512 # (and tag it to do more later)
7513 if ($data =~ s/^\s*(\{)\s*['"]statuses['"]\s*:\s*(\[.*\]).*$/$2/isg) {
7514 $kludge_search_api_adjust = 1;
7515 }
7516
7517 $my_json_ref = &parsejson($data);
7518
7519 # normalize the data into a standard form.
7520 # single tweets such as from statuses/show aren't arrays, so
7521 # we special-case for them.
7522 if (defined($my_json_ref) && ref($my_json_ref) eq 'HASH' &&
7523 ((0+$my_json_ref->{'id'}) ||
7524 length($my_json_ref->{'id_str'}))) {
7525 $my_json_ref = &normalizejson($my_json_ref);
7526 }
7527 if (defined($my_json_ref) && ref($my_json_ref) eq 'ARRAY') {
7528 foreach $i (@{ $my_json_ref }) {
7529 $i = &normalizejson($i,$kludge_search_api_adjust,$tag);
7530 }
7531 }
7532
7533 $laststatus = 0;
7534 return $my_json_ref;
7535 }
7536
7537 # convert t.co into actual URLs. separate from normalizejson because other
7538 # things need this. modified from /entities.
7539 sub destroy_all_tco {
7540 my $hash = shift;
7541 return $hash if ($notco);
7542 my $v;
7543 my $type;
7544
7545 # Twitter puts entities in multiple fields.
7546 # TODO: For old-style retweets should manipulate and revert back to t.co links
7547 # Note: The search api does not include extended_entities
7548 # Do extended first to get video urls, otherwise we'll just get a thumbnail
7549 my (@entities_fields) = ($hash->{extended_entities}, $hash->{entities});
7550 if ($extended && exists $hash->{extended_tweet}) {
7551 push @entities_fields, $hash->{extended_tweet}->{entities};
7552 push @entities_fields, $hash->{extended_tweet}->{extended_entities};
7553 }
7554 foreach my $entities_field (@entities_fields) {
7555 foreach $type (qw(media urls)) {
7556 my $urls;
7557 my $u1;
7558 my $array = $entities_field->{$type};
7559 next if (!defined($array) || ref($array) ne 'ARRAY');
7560 foreach $entry (@{ $array }) {
7561 next if (!defined($entry) || ref($entry) ne 'HASH');
7562 next if (!length($entry->{'url'}) ||
7563 (!length($entry->{'expanded_url'}) &&
7564 !length($entry->{'media_url'})));
7565 # There is one canonical url even for multiple media (picture) entries
7566 $u1 = $u1 || quotemeta($entry->{'url'});
7567 if (defined($entry->{'video_info'})) {
7568 # Need to look for content_type, prefer mp4 since that's more
7569 # broadly viewable, but accept m3u8 because that's where
7570 # Twitter is going with videos.
7571 my $videourl = '';
7572 #foreach $variant (@{ $entry->{'video_info'}->{'variants'} }) {
7573 # if ($variant->{'content_type'} =~ /mp4/) {
7574 # $videourl = $variant->{'url'};
7575 # last;
7576 # } elsif (($variant->{'content_type'} =~ /x-mpegURL/) || (! $videourl)) {
7577 # $videourl = $variant->{'url'};
7578 # }
7579 #}
7580 my %mp4_variants = ();
7581 my %m3u8_variants = ();
7582 my @videos = ();
7583 foreach $variant (@{ $entry->{'video_info'}->{'variants'} }) {
7584 $mp4_variants{$variant->{'url'}} = $variant if ($variant->{'content_type'} =~ /mp4/ );
7585 $m3u8_variants{$variant->{'url'}} = $variant if ($variant->{'content_type'} =~ /x-mpegURL/ );
7586 }
7587 if ( %mp4_variants ) {
7588 @videos = sort { $mp4_variants{$a}->{bitrate} <=> $mp4_variants{$b}->{bitrate} } keys %mp4_variants;
7589 } else {
7590 @videos = sort { $m3u8_variants{$a}->{bitrate} <=> $m3u8_variants{$b}->{bitrate} } keys %m3u8_variants;
7591 }
7592 if ( $video_bitrate eq 'highest' ) {
7593 $videourl = $videos[-1];
7594 } else {
7595 $videourl = $videos[0];
7596 }
7597 $urls = $urls . " " . $videourl;
7598 $urls = strim($urls);
7599 }
7600 else {
7601 my $tempurls = $entry->{'media_url_https'} || $entry->{'media_url'} || $entry->{'expanded_url'};
7602 $urls = $urls . " " . $tempurls;
7603 $urls = strim($urls);
7604 }
7605 if ($type eq 'urls') {
7606 # Need to replace now and reset urls
7607 if ($urls ne "") {
7608 # Let's play safe and only replace the tco if we have something to replace it with
7609 $hash->{'text'} =~ s/$u1/$urls/;
7610 }
7611 $urls = "";
7612 $u1 = "";
7613 }
7614 }
7615 if ($type eq 'media') {
7616 # Then we need to replace outside of the above loop since one tco for all media entries
7617 if ($urls ne "") {
7618 # Let's play safe and only replace the tco if we have something to replace it with
7619 $hash->{'text'} =~ s/$u1/$urls/;
7620 }
7621 }
7622 }
7623 }
7624 return $hash;
7625 }
7626
7627 # takes a tweet structure and normalizes it according to settings.
7628 # what this currently does is the following gyrations:
7629 # - if there is no id_str, see if we can convert id into one. if
7630 # there is loss of precision, warn the user. same for
7631 # in_reply_to_status_id_str.
7632 # - if the source of this JSON data source is the Search API, translate
7633 # its fields into the standard API.
7634 # - if the calling function has specified a tag, tag the tweets, since
7635 # we're iterating through them anyway. the tag should be a hashref payload.
7636 # - if the tweet is an newRT, unwrap it so that the full tweet text is
7637 # revealed (unless -nonewrts).
7638 # - if this appears to be a tweet, put in a stub geo hash if one does
7639 # not yet exist.
7640 # - if coordinates are flat string 'null', turn into a real null.
7641 # - if $extended is on and the tweet has an extended_tweet field, promote
7642 # full_text from extended_tweet to the top level
7643 # one day I would like this code to go the hell away.
7644 sub normalizejson {
7645 my $i = shift;
7646 my $kludge_search_api_adjust = shift;
7647 my $tag = shift;
7648 my $rt;
7649
7650 # tag the tweet
7651 $i->{'tag'} = $tag if (defined($tag));
7652
7653 # id -> id_str if needed
7654 if (!length($i->{'id_str'})) {
7655 my $k = "" + (0 + $i->{'id'});
7656 if ($k !~ /[eE][+-]/) {
7657 $i->{'id_str'} = $k;
7658 } else {
7659 # desperately try to convert
7660 $k =~ s/[eE][+-]\d+$//;
7661 $k =~ s/\.//g;
7662 # this is a hack, so we warn.
7663 &$exception(13,
7664 "*** impending doom: ID overflows Perl precision; stubbed to $k\n");
7665 $i->{'id_str'} = $k;
7666 }
7667 }
7668 # irtsid -> irtsid_str (if there is one)
7669 if (!length($i->{'in_reply_to_status_id_str'}) &&
7670 $i->{'in_reply_to_status_id'}) {
7671 my $k = "" + (0+$i->{'in_reply_to_status_id'});
7672 if ($k !~ /[eE][+-]/) {
7673 $i->{'in_reply_to_status_id_str'} = $k;
7674 } else {
7675 # desperately try to convert
7676 $k =~ s/[eE][+-]\d+$//;
7677 $k =~ s/\.//g;
7678 # this is a hack, so we warn.
7679 &$exception(13,
7680 "*** impending doom: IRT-ID overflows Perl precision; stubbed to $k\n");
7681 $i->{'in_reply_to_status_id_str'} = $k;
7682 }
7683 }
7684
7685 # normalize geo. if this has a source and it has a
7686 # favorited, then it is probably a tweet and we will
7687 # add a stub geo hash if one doesn't exist yet.
7688 if ($kludge_search_api_adjust ||
7689 ($i->{'liked'} && $i->{'source'})){
7690 $i = &fix_geo_api_data($i);
7691 }
7692
7693 # hooray! this just tags it
7694 if ($kludge_search_api_adjust) {
7695 $i->{'class'} = "search";
7696 }
7697
7698 # normalize extended tweets
7699 # We probably ought to handle the other fields in extended_tweet,
7700 # but this will also all go away once compatibility mode does
7701 if ($extended) {
7702 if (exists $i->{'extended_tweet'}) {
7703 $i->{'text'} = $i->{'extended_tweet'}->{'full_text'};
7704 } elsif (exists $i->{'full_text'}) {
7705 $i->{'text'} = $i->{'full_text'};
7706 }
7707 }
7708
7709 # normalize newRTs
7710 # if we get newRTs with -nonewrts, oh well
7711 if (!$nonewrts && ($rt = $i->{'retweeted_status'})) {
7712 # reconstruct the RT in a "canonical" format
7713 # without truncation, but detco it first
7714 if ($extended) {
7715 if (exists $rt->{'extended_tweet'}) {
7716 $rt->{'text'} = $rt->{'extended_tweet'}->{'full_text'};
7717 } elsif (exists $rt->{'full_text'}) {
7718 $rt->{'text'} = $rt->{'full_text'};
7719 }
7720 }
7721 $rt = &destroy_all_tco($rt);
7722 $rt = &fix_geo_api_data($rt);
7723
7724 $i->{'retweeted_status'} = $rt;
7725 $i->{'text'} =
7726 "RT \@$rt->{'user'}->{'screen_name'}" . ': ' . $rt->{'text'};
7727 #Nested quote tweets, since displaying those
7728 if ($qt = $i->{'retweeted_status'}->{'quoted_status'}) {
7729 if ($extended) {
7730 if (exists $qt->{'extended_tweet'}) {
7731 $qt->{'text'} = $qt->{'extended_tweet'}->{'full_text'};
7732 } elsif (exists $qt->{'full_text'}) {
7733 $qt->{'text'} = $qt->{'full_text'};
7734 }
7735 }
7736 $qt = &destroy_all_tco($qt);
7737 $qt = &fix_geo_api_data($qt);
7738
7739 $i->{'retweeted_status'}->{'quoted_status'} = $qt;
7740 }
7741 }
7742 # normalize quote tweets
7743 if ($qt = $i->{'quoted_status'}) {
7744 if ($extended) {
7745 if (exists $qt->{'extended_tweet'}) {
7746 $qt->{'text'} = $qt->{'extended_tweet'}->{'full_text'};
7747 } elsif (exists $qt->{'full_text'}) {
7748 $qt->{'text'} = $qt->{'full_text'};
7749 }
7750 }
7751 $qt = &destroy_all_tco($qt);
7752 $qt = &fix_geo_api_data($qt);
7753
7754 $i->{'quoted_status'} = $qt;
7755 }
7756
7757 return &destroy_all_tco($i);
7758 }
7759
7760 # process the JSON data ... simplemindedly, because I just write utter crap,
7761 # am not a professional programmer, and don't give a flying fig whether
7762 # kludges suck or no. this used to be part of grabjson, but I split it out.
7763 sub parsejson {
7764 my $data = shift;
7765 my $my_json_ref = undef; # durrr hat go on foot
7766 my $i;
7767 my $tdata;
7768 my $seed;
7769 my $bbqqmask;
7770 my $ddqqmask;
7771 my $ssqqmask;
7772
7773 # test for single logicals
7774 return {
7775 'ok' => 1,
7776 'result' => (($1 eq 'true') ? 1 : 0),
7777 'literal' => $1,
7778 } if ($data =~ /^['"]?(true|false)['"]?$/);
7779
7780 # first isolate escaped backslashes with a unique sequence.
7781 $bbqqmask = "BBQQ";
7782 $seed = 0;
7783 $seed++ while ($data =~ /$bbqqmask$seed/);
7784 $bbqqmask .= $seed;
7785 $data =~ s/\\\\/$bbqqmask/g;
7786
7787 # next isolate escaped quotes with another unique sequence.
7788 $ddqqmask = "DDQQ";
7789 $seed = 0;
7790 $seed++ while ($data =~ /$ddqqmask$seed/);
7791 $ddqqmask .= $seed;
7792 $data =~ s/\\\"/$ddqqmask/g;
7793
7794 # then turn literal ' into another unique sequence. you'll see
7795 # why momentarily.
7796 $ssqqmask = "SSQQ";
7797 $seed = 0;
7798 $seed++ while ($data =~ /$ssqqmask$seed/);
7799 $ssqqmask .= $seed;
7800 $data =~ s/\'/$ssqqmask/g;
7801
7802 # here's why: we're going to turn doublequoted strings into single
7803 # quoted strings to avoid nastiness like variable interpolation.
7804 $data =~ s/\"/\'/g;
7805
7806 # and then we're going to turn the inline ones all back except
7807 # ssqq, which we'll do last so that our syntax checker still works.
7808 $data =~ s/$bbqqmask/\\\\/g;
7809 $data =~ s/$ddqqmask/"/g;
7810
7811 print $stdout "$data\n" if ($superverbose);
7812
7813 # trust, but verify. I'm sure twitter wouldn't send us malicious
7814 # or bogus JSON, but one day this might talk to something that would.
7815 # in particular, need to make sure nothing in this will eval badly or
7816 # run arbitrary code. that would really suck!
7817 # first, generate a syntax tree.
7818 $tdata = $data;
7819 1 while $tdata =~ s/'[^']*'//; # empty strings are valid too ...
7820 $tdata =~ s/-?[0-9]+\.?[0-9]*([eE][+-][0-9]+)?//g;
7821 # have to handle floats *and* their exponents
7822 $tdata =~ s/(true|false|null)//g;
7823 $tdata =~ s/\s//g;
7824
7825 print $stdout "$tdata\n" if ($superverbose);
7826
7827 # now verify the syntax tree.
7828 # the remaining stuff should just be enclosed in [ ], and only {}:,
7829 # for example, imagine if a bare semicolon were in this ...
7830 if ($tdata !~ s/^\[// || $tdata !~ s/\]$// || $tdata =~ /[^{}:,]/) {
7831 $tdata =~ s/'[^']*$//; # cut trailing strings
7832 if (($tdata =~ /^\[/ && $tdata !~ /\]$/)
7833 || ($tdata =~ /^\{/ && $tdata !~ /\}$/)) {
7834 # incomplete transmission
7835 &$exception(10, "*** JSON warning: connection cut\n");
7836 return undef;
7837 }
7838 # it seems that :[], or :[]} should be accepted as valid in the syntax tree
7839 # since identica uses this as possible for null properties
7840 # ,[], shouldn't be, etc.
7841 if ($tdata =~ /(^|[^:])\[\]($|[^},])/) { # oddity
7842 &$exception(11, "*** JSON warning: null list\n");
7843 return undef;
7844 }
7845 # at this point all we should have are structural elements.
7846 # if something other than JSON structure is visible, then
7847 # the syntax tree is mangled. don't try to run it, it
7848 # might be unsafe. this exception was formerly uniformly
7849 # fatal. it is now non-fatal as of 2.1.
7850 if ($tdata =~ /[^\[\]\{\}:,]/) {
7851 &$exception(99, "*** JSON syntax error\n");
7852 print $stdout <<"EOF" if ($verbose);
7853 --- data received ---
7854 $data
7855 --- syntax tree ---
7856 $tdata
7857 --- JSON PARSING ABORTED DUE TO SYNTAX TREE FAILURE --
7858 EOF
7859 return undef;
7860 }
7861 }
7862
7863 # syntax tree passed, so let's turn it into a Perl reference.
7864 # have to turn colons into ,s or Perl will gripe. but INTELLIGENTLY!
7865 1 while
7866 ($data =~ s/([^'])'\s*:\s*(true|false|null|\'|\{|\[|-?[0-9])/\1\',\2/);
7867
7868 # finally, single quotes, just before interpretation.
7869 $data =~ s/$ssqqmask/\\'/g;
7870
7871 # now somewhat validated, so safe (?) to eval() into a Perl struct
7872 eval "\$my_json_ref = $data;";
7873 print $stdout "$data => $my_json_ref $@\n" if ($superverbose);
7874
7875 # do a sanity check
7876 if (!defined($my_json_ref)) {
7877 &$exception(99, "*** JSON syntax error\n");
7878 print $stdout <<"EOF" if ($verbose);
7879 --- data received ---
7880 $data
7881 --- syntax tree ---
7882 $tdata
7883 --- JSON PARSING FAILED --
7884 $@
7885 --- JSON PARSING FAILED --
7886 EOF
7887 }
7888
7889 return $my_json_ref;
7890 }
7891
7892 sub fix_geo_api_data {
7893 my $ref = shift;
7894 $ref->{'geo'}->{'coordinates'} = undef
7895 if ($ref->{'geo'}->{'coordinates'} eq 'null' ||
7896 $ref->{'geo'}->{'coordinates'}->[0] eq '' ||
7897 $ref->{'geo'}->{'coordinates'}->[1] eq '');
7898 $ref->{'geo'}->{'coordinates'} ||= [ "undef", "undef" ];
7899 return $ref;
7900 }
7901
7902 sub is_fail_whale {
7903 # is this actually the dump from a fail whale?
7904 my $data = shift;
7905 return ($data =~ m#<title>Twitter.+Over.+capacity.*</title>#i ||
7906 $data =~ m#[\r\l\n\s]*DB_DataObject Error: Connect failed#s);
7907 }
7908
7909 # {'errors':[{'message':'Rate limit exceeded','code':88}]}
7910 sub is_json_error {
7911 # is this actually a JSON error message? if so, extract it
7912 my $data = shift;
7913 if ($data =~ /(['"])(warning|errors?)\1\s*:\s*/s) {
7914 if ($data =~ /^\s*\{/s) { # JSON object?
7915 my $dref = &parsejson($data);
7916 print $stdout "*** is_json_error returning true\n"
7917 if ($verbose);
7918 # support 1.0 and 1.1 error objects
7919 return $dref->{'error'} if (length($dref->{'error'}));
7920 return $dref->{'errors'}->[0]->{'message'}
7921 if (length($dref->{'errors'}->[0]->{'message'}));
7922 return (split(/\\n/, $dref->{'errors'}))[0]
7923 if(length($dref->{'errors'}));
7924 }
7925 return $data;
7926 }
7927 return undef;
7928 }
7929
7930 sub backticks {
7931 # more efficient/flexible backticks system
7932 my $comm = shift;
7933 my $rerr = shift;
7934 my $rout = shift;
7935 my $resource = shift;
7936 my $data = shift;
7937 my $dont_do_auth = shift;
7938 my $buf = '';
7939 my $undersave = $_;
7940 my $pid;
7941 my $args;
7942
7943 ($comm, $args, $data) = &$stringify_args($comm, $resource,
7944 $data, $dont_do_auth, @_);
7945 print $stdout "$comm\n$args\n$data\n" if ($superverbose);
7946 if(open(BACTIX, '-|')) {
7947 while(<BACTIX>) {
7948 $buf .= $_;
7949 } close(BACTIX);
7950 $_ = $undersave;
7951 return $buf; # and $? is still in $?
7952 } else {
7953 $in_backticks = 1;
7954 &sigify(sub {
7955 die(
7956 "** user agent not honouring timeout (caught by sigalarm)\n");
7957 }, qw(ALRM));
7958 alarm 120; # this should be sufficient
7959 if (length($rerr)) {
7960 close(STDERR);
7961 open(STDERR, ">$rerr");
7962 }
7963 if (length($rout)) {
7964 close(STDOUT);
7965 open(STDOUT, ">$rout");
7966 }
7967 if(open(FRONTIX, "|$comm")) {
7968 print FRONTIX "$args\n";
7969 print FRONTIX "$data" if (length($data));
7970 close(FRONTIX);
7971 } else {
7972 die(
7973 "backticks() failure for $comm $rerr $rout @_: $!\n");
7974 }
7975 $rv = $? >> 8;
7976 exit $rv;
7977 }
7978 }
7979
7980 sub wherecheck {
7981 my ($prompt, $filename, $fatal) = (@_);
7982 my (@paths) = split(/\:/, $ENV{'PATH'});
7983 my $setv = '';
7984
7985 push(@paths, '/usr/bin'); # the usual place
7986 @paths = ('') if ($filename =~ m#^/#); # for absolute paths
7987
7988 print $stdout "$prompt ... " unless ($silent);
7989 foreach(@paths) {
7990 if (-r "$_/$filename") {
7991 $setv = "$_/$filename";
7992 1 while $setv =~ s#//#/#;
7993 print $stdout "$setv\n" unless ($silent);
7994 last;
7995 }
7996 }
7997 if (!length($setv)) {
7998 print $stdout "not found.\n";
7999 if ($fatal) {
8000 print $stdout $fatal;
8001 exit(1);
8002 }
8003 }
8004 return $setv;
8005 }
8006
8007 sub screech {
8008 print $stdout "\n\n${BEL}${BEL}@_";
8009 if ($is_background) {
8010 kill 9, $parent;
8011 kill 9, $$;
8012 } elsif ($child) {
8013 kill 9, $child;
8014 kill 9, $$;
8015 }
8016 die("death not achieved conventionally");
8017 }
8018
8019 # &in($x, @y) returns true if $x is a member of @y
8020 sub in { my $key = shift; my %mat = map { $_ => 1 } @_;
8021 return $mat{$key}; }
8022
8023 sub descape {
8024 my $x = shift;
8025 my $mode = shift;
8026
8027 $x =~ s#\\/#/#g;
8028
8029 # try to do something sensible with unicode
8030 if ($mode) { # this probably needs to be revised
8031 $x =~ s/\\u([0-9a-fA-F]{4})/"&#" . hex($1) . ";"/eg;
8032 } else {
8033 # intermediate form if HTML entities get in
8034 $x =~ s/\&\#([0-9]+);/'\u' . sprintf("%04x", $1)/eg;
8035
8036 $x =~ s/\\u202[89]/\\n/g;
8037
8038 # canonicalize Unicode whitespace
8039 1 while ($x =~ s/\\u(00[aA]0)/ /g);
8040 1 while ($x =~ s/\\u(200[0-9aA])/ /g);
8041 1 while ($x =~ s/\\u(20[25][fF])/ /g);
8042 if ($seven) {
8043 # known UTF-8 entities (char for char only)
8044 $x =~ s/\\u201[89]/\'/g;
8045 $x =~ s/\\u201[cCdD]/\"/g;
8046
8047 # 7-bit entities (32-126) also ok
8048 $x =~ s/\\u00([2-7][0-9a-fA-F])/chr(((hex($1)==127)?46:hex($1)))/eg;
8049
8050 # dot out the rest
8051 $x =~ s/\\u([0-9a-fA-F]{4})/./g;
8052 $x =~ s/[\x80-\xff]/./g;
8053 } else {
8054 # try to promote to UTF-8
8055 &$utf8_decode($x);
8056
8057 # Twitter uses UTF-16 for high code points, which
8058 # Perl's UTF-8 support does not like as surrogates.
8059 # try to decode these here; they are always back-to-
8060 # back surrogates of the form \uDxxx\uDxxx
8061 $x =~
8062 s/\\u([dD][890abAB][0-9a-fA-F]{2})\\u([dD][cdefCDEF][0-9a-fA-F]{2})/&deutf16($1,$2)/eg;
8063
8064 # decode the rest
8065 $x =~ s/\\u([0-9a-fA-F]{4})/chr(hex($1))/eg;
8066 $x = &uforcemulti($x);
8067 }
8068 $x =~ s/\&quot;/"/g;
8069 $x =~ s/\&apos;/'/g;
8070 $x =~ s/\&lt;/\</g;
8071 $x =~ s/\&gt;/\>/g;
8072 $x =~ s/\&amp;/\&/g;
8073 }
8074 # TODO: Here it doesn't seem possible for us to distinguish between real newlines and the literal "\"s followed by "n"s that may have been sent and both will get replaced. But it would be nice to investigate this further.
8075 if ($newline eq "replace") {
8076 $x =~ s/\\n/$replacement_newline/sg;
8077 $x =~ s/\\r/$replacement_carriagereturn/sg;
8078 }
8079 elsif ($newline) {
8080 $x =~ s/\\n/\n/sg;
8081 $x =~ s/\\r//sg;
8082 }
8083 return $x;
8084 }
8085
8086 # used by descape: turn UTF-16 surrogates into a Unicode character
8087 sub deutf16 {
8088 my $one = hex(shift);
8089 my $two = hex(shift);
8090 # subtract 55296 from $one to yield top ten bits
8091 $one -= 55296; # $d800
8092 # subtract 56320 from $two to yield bottom ten bits
8093 $two -= 56320; # $dc00
8094
8095 # experimentally, Twitter uses this endianness below (we have no BOM)
8096 # see RFC 2781 4.3
8097 return chr(($one << 10) + $two + 65536);
8098 }
8099 sub max { return ($_[0] > $_[1]) ? $_[0] : $_[1]; }
8100 sub min { return ($_[0] < $_[1]) ? $_[0] : $_[1]; }
8101 sub prolog { my $k = shift;
8102 return "" if (!scalar(@_));
8103 my $l = shift; return (&$k($l) . &$k(@_)); }
8104 # this is mostly a utility function for /eval. it is a recursive descent
8105 # pretty printer.
8106 sub a {
8107 my $w;
8108 my $x;
8109 return '' if(scalar(@_) < 1);
8110 if(scalar(@_) > 1) { $x = "(";
8111 foreach $w (@_) {
8112 $x .= &a($w);
8113 }
8114 return $x."), ";
8115 }
8116 $w = shift;
8117 if(ref($w) eq 'SCALAR') { return "\\\"". $$w . "\", "; }
8118 if(ref($w) eq 'HASH') { my %m = %{ $w };
8119 return "\n\t{".&prolog(\&a, %m)."}, "; }
8120 if(ref($w) eq 'ARRAY') { return "\n\t[".&prolog(\&a, @{ $w })."], "; }
8121 return "\"$w\", ";
8122 }
8123 sub ssa { return (scalar(@_) ? ("('" . join("', '", @_) . "')") : "NULL"); }
8124
8125 sub strim { my $x=shift; $x=~ s/^\s+//; $x=~ s/\s+$//; return $x; }
8126
8127 sub wwrap {
8128 return shift if (!$wrap);
8129
8130 my $k;
8131 my $klop = ($wrap > 1) ? $wrap : ($ENV{'COLUMNS'} || 79);
8132 $klop--; # don't ask me why
8133 my $lop;
8134 my $buf = '';
8135 my $string = shift;
8136 my $indent = shift; # for very first time with the prompt
8137 my $needspad = 0;
8138 my $stringpad = " " x 3;
8139
8140 $indent += 4; # for the menu select string
8141
8142 $lop = $klop - $indent;
8143 $lop -= $indent;
8144 W: while($k = length($string)) {
8145 $lop += $indent if ($lop < $klop);
8146 ($buf .= $string, last W) if ($k <= $lop && $string !~ /\n/);
8147 ($string =~ s/^\s*\n//) && ($buf .= "\n",
8148 $needspad = 1,
8149 next W);
8150 if ($needspad) {
8151 $string = " $string";
8152 $needspad = 0;
8153 }
8154 # I don't know if people will want this, so it's commented out.
8155 #($string =~ s#^(http://[^\s]+)# #) && ($buf .= "$1\n",
8156 # next W);
8157 ($string =~ s/^(.{4,$lop})\s/ /) && ($buf .= "$1\n",
8158 next W); # i.e., at least one char, plus 3 space indent
8159 ($string =~ s/^(.{$lop})/ /) && ($buf .= "$1\n",
8160 next W);
8161 warn
8162 "-- pathologic string somehow failed wordwrap! \"$string\"\n";
8163 return $buf;
8164 }
8165 1 while ($buf =~ s/\n\n\n/\n\n/s); # mostly paranoia
8166 $buf =~ s/[ \t]+$//;
8167 return $buf;
8168 }
8169
8170 # these subs look weird, but they're encoding-independent and run anywhere
8171 sub uforcemulti { # forces multi-byte interpretation by abusing Perl
8172 my $x = shift;
8173 return $x if ($seven);
8174 $x = "\x{263A}".$x;
8175 return pack("${pack_magic}H*", substr(unpack("${pack_magic}H*",$x),6));
8176 }
8177 sub ulength { my @k; return (scalar(@k = unpack("${pack_magic}C*", shift))); }
8178 sub uhex {
8179 # URL-encode an arbitrary string, even UTF-8
8180 # more versatile than the miniature one in &updatest
8181 my $k = '';
8182 my $s = shift;
8183 &$utf8_encode($s);
8184
8185 foreach(split(//, $s)) {
8186 my $j = unpack("H256", $_);
8187 while(length($j)) {
8188 $k .= '%' . substr($j, 0, 2);
8189 $j = substr($j, 2);
8190 }
8191 }
8192 return $k;
8193 }
8194
8195 # for t.co
8196 # adapted from github.com/twitter/twitter-text-js/blob/master/twitter-text.js
8197 # this is very hard to get right, and I know there are edge cases. this first
8198 # one is designed to be quick and dirty because it needs to be fast more than
8199 # it needs to be accurate, since T:RL:T calls it a LOT. however, it can be
8200 # fooled, see below.
8201 sub fastturntotco {
8202 my $s = shift;
8203 my $w;
8204
8205 # turn domain names into http urls. this should look at .com, .net,
8206 # .etc., but things like you.suck.too probably *should* hit this
8207 # filter. this uses the heuristic that a domain name over some limit
8208 # is probably not actually a domain name.
8209 ($s =~ s#\b(([a-zA-Z0-9-_]\.)+([a-zA-Z]){2,})\b#((length($w="$1")>45)?$w:"http://$w")#eg);
8210
8211 # now turn all http and https URLs into t.co strings
8212 my $tco_string = 'X' x ( $tco_length - 13 );
8213 ($s =~ s#\b(https?)://[a-zA-Z0-9-_]+[^\s]*?('|\\|\s|[\.;:,!\?]\s+|[\.;:,!\?]$|$)#https://t.co/${tco_string}\2#gi);
8214 return $s;
8215 }
8216 # slow t.co converter. this is for future expansion.
8217 sub turntotco {
8218 return &fastturntotco(shift);
8219 }
8220
8221 sub ulength_tco {
8222 my $w = shift;
8223 return &ulength(($notco) ? $w : &turntotco($w));
8224 }
8225 sub length_tco {
8226 my $w = shift;
8227 return length_newline(($notco) ? $w : &turntotco($w));
8228 }
8229 sub length_newline {
8230 # Count length of a string, adjusting for sending newlines
8231 my $s = shift;
8232 my @count_of_newlines;
8233 my @count_of_liternal_newlines;
8234
8235 # Count number of \n and \\n
8236 @count_of_newlines = ($s =~ /\\n/g);
8237 @count_of_literal_newlines = ($s =~ /\\\\n/g);
8238 # \n only count as one character so subtract one for each count
8239 # \\n only counts as two characters so subtract one for each count
8240 return length($s)-scalar(@count_of_newlines)-scalar(@count_of_literal_newlines);
8241 }
8242 # take a string and return up to $maxchars CHARS plus the rest.
8243 sub csplit {
8244 my ($orig_k, $autosplit, $maxchars) = @_;
8245 return &cosplit($orig_k, $autosplit, $maxchars, sub { return &length_tco(shift); });
8246 }
8247 # take a string and return up to $linelength BYTES plus the rest.
8248 # usplit isn't used, but best change it as well
8249 sub usplit {
8250 my ($orig_k, $autosplit, $maxchars) = @_;
8251 return &cosplit($orig_k, $autosplit, $maxchars, sub { return &ulength_tco(shift); });
8252 }
8253 sub cosplit {
8254 # this is the common code for &csplit and &usplit.
8255 # this is tricky because we don't want to split up UTF-8 sequences, so
8256 # we let Perl do the work since it internally knows where they end.
8257 my $orig_k = shift;
8258 my $autosplit = shift;
8259 my $maxchars = shift;
8260 my $lengthsub = shift;
8261 my $z;
8262 my @m;
8263 my $q;
8264 my $r;
8265
8266 unless ($maxchars) {
8267 $maxchars = $linelength;
8268 }
8269
8270 my $mode = ($autosplit eq 'char' || $autosplit eq 'cut') ? 1 : 0;
8271 $k = $orig_k;
8272
8273 # optimize whitespace
8274 $k =~ s/^\s+//;
8275 $k =~ s/\s+$//;
8276 $k =~ s/\s+/ /g;
8277 $z = &$lengthsub($k);
8278 return ($k) if ($z <= $maxchars); # also handles the trivial case
8279
8280 # this needs to be reply-aware, so we put @'s at the beginning of
8281 # the second half too (and also Ds for DMs)
8282 $r .= $1 while ($k =~ s/^(\@[^\s]+\s)\s*// ||
8283 $k =~ s/^(D\s+[^\s]+\s)\s*//); # we have r/a, so while
8284 $k = "$r$k";
8285
8286 my $i = $maxchars;
8287 $i-- while(($z = &$lengthsub($q = substr($k, 0, $i))) > $maxchars);
8288 $m = substr($k, $i);
8289
8290 # if we just wanted split-on-byte, return now (mode = 1)
8291 if ($mode) {
8292 # optimize again in case we split on whitespace
8293 $q =~ s/\s+$//;
8294 $m =~ s/^\s+//;
8295 return ($q, "$r$m");
8296 }
8297
8298 # else try to do word boundary and cut even more
8299 if (!$autosplit) { # use old mechanism first: drop trailing non-alfanum
8300 ($q =~ s/([^a-zA-Z0-9]+)$//) && ($m = "$1$m");
8301 # optimize again in case we split on whitespace
8302 $q =~ s/\s+$//;
8303 return (&cosplit($orig_k, "cut", $lengthsub))
8304 #Don't need to use length_newline here becausen only checking for whether zero length is true
8305 if (!length($q) && !$mode);
8306 # it totally failed. fall back on charsplit.
8307 if (&$lengthsub($q) < $maxchars) {
8308 $m =~ s/^\s+//;
8309 return($q, "$r$m")
8310 }
8311 }
8312 ($q =~ s/\s+([^\s]+)$//) && ($m = "$1$m");
8313 #Don't need to use length_newline here because only checking for whether zero length is true
8314 return (&cosplit($orig_k, "cut", $lengthsub)) if (!length($q) && !$mode);
8315 # it totally failed. fall back on charsplit.
8316 return ($q, "$r$m");
8317 }
8318
8319 ### OAuth methods, including our own homegrown SHA-1 and HMAC ###
8320 ### no Digest:* required! ###
8321 ### these routines are not byte-safe and need a use bytes; before you call ###
8322
8323 # this is a modified, deciphered and deobfuscated version of the famous Perl
8324 # one-liner SHA-1 written by John Allen. hope he doesn't mind.
8325 sub sha1 {
8326 my $string = shift;
8327 print $stdout "string length: @{[ length($string) ]}\n"
8328 if ($showwork);
8329
8330 my $constant = "D9T4C`>_-JXF8NMS^\$#)4=L/2X?!:\@GF9;MGKH8\\;O-S*8L'6";
8331 my @A = unpack('N*', unpack('u', $constant));
8332 my @K = splice(@A, 5, 4);
8333 my $M = sub { # 64-bit warning
8334 my $x;
8335 my $m;
8336 ($x = pop @_) - ($m=4294967296) * int($x / $m);
8337 };
8338 my $L = sub { # 64-bit warning
8339 my $n = pop @_;
8340 my $x;
8341 ((($x = pop @_) << $n) | ((2 ** $n - 1) & ($x >> 32 - $n))) &
8342 4294967295;
8343 };
8344 my $l = '';
8345 my $r;
8346 my $a;
8347 my $b;
8348 my $c;
8349 my $d;
8350 my $e;
8351 my $us;
8352 my @nuA;
8353 my $p = 0;
8354 $string = unpack("H*", $string);
8355
8356 do {
8357 my $i;
8358 $us = substr($string, 0, 128);
8359 $string = substr($string, 128);
8360 $l += $r = (length($us) / 2);
8361 print $stdout "pad length: $r\n" if ($showwork);
8362 ($r++, $us .= "80") if ($r < 64 && !$p++);
8363 my @W = unpack('N16', pack("H*", $us) . "\000" x 7);
8364 $W[15] = $l * 8 if ($r < 57);
8365 foreach $i (16 .. 79) {
8366 push(@W,
8367 &$L($W[$i - 3] ^ $W[$i - 8] ^ $W[$i - 14] ^ $W[$i - 16], 1));
8368 }
8369 ($a, $b, $c, $d, $e) = @A;
8370 foreach $i (0 .. 79) {
8371 my $qq = ($i < 20) ? ($b & ($c ^ $d) ^ $d) :
8372 ($i < 40) ? ($b ^ $c ^ $d) :
8373 ($i < 60) ? (($b | $c) & $d | $b & $c) :
8374 ($b ^ $c ^ $d);
8375 $t = &$M($qq + $e + $W[$i] + $K[$i / 20] + &$L($a, 5));
8376 $e = $d;
8377 $d = $c;
8378 $c = &$L($b, 30);
8379 $b = $a;
8380 $a = $t;
8381 }
8382 @nuA = ($a, $b, $c, $d, $e);
8383 print $stdout "$a $b $c $d $e\n" if ($showwork);
8384 $i = 0;
8385 @A = map({ &$M($_ + $nuA[$i++]); } @A);
8386 } while ($r > 56);
8387 my $x = sprintf('%.8x' x 5, @A);
8388 @A = unpack("C*", pack("H*", $x));
8389 return($x, @A);
8390 }
8391
8392 # heavily modified from MIME::Base64
8393 sub simple_encode_base64 {
8394 my $result = '';
8395 my $input = shift;
8396
8397 pos($input) = 0;
8398 while($input =~ /(.{1,45})/gs) {
8399 $result .= substr(pack("u", $1), 1);
8400 chop($result);
8401 }
8402 $result =~ tr|` -_|AA-Za-z0-9+/|;
8403 my $padding = (3 - length($input) % 3) % 3;
8404 $result =~ s/.{$padding}$/("=" x $padding)/e if ($padding);
8405
8406 return $result;
8407 }
8408
8409 # from RFC 2104/RFC 2202
8410
8411 sub hmac_sha1 {
8412 my $message = shift;
8413 my @key = (@_);
8414 my $opad;
8415 my $ipad;
8416 my $i;
8417 my @j;
8418
8419 # sha1 blocksize is 512, so key should be 64 bytes
8420
8421 print $stdout " KEY HASH \n" if ($showwork);
8422 ($i, @key) = &sha1(pack("C*", @key)) while (scalar(@key) > 64);
8423 push(@key, 0) while(scalar(@key) < 64);
8424 $opad = pack("C*", map { ($_ ^ 92) } @key);
8425 $ipad = pack("C*", map { ($_ ^ 54) } @key);
8426
8427 print $stdout " MESSAGE HASH \n" if ($showwork);
8428 ($i, @j) = &sha1($ipad . $message);
8429 print $stdout " FINAL HASH \n" if ($showwork);
8430 $i = pack("C*", @j); # output hash is 160 bits
8431 ($i, @j) = &sha1($opad . $i);
8432 $i = &simple_encode_base64(pack("C20", @j));
8433
8434 return $i;
8435 }
8436
8437 # simple encoder for OAuth modified URL encoding (used for lots of things,
8438 # actually)
8439 # this is NOT UTF-8 safe
8440 sub url_oauth_sub {
8441 my $x = shift;
8442 $x =~ s/([^-0-9a-zA-Z._~])/"%".uc(unpack("H*",$1))/eg; return $x;
8443 }
8444
8445 # default method of getting password: ask for it. only relevant for Basic Auth,
8446 # which is no longer the default.
8447 sub defaultgetpassword {
8448 # original idea by @jcscoobyrs, heavily modified
8449 my $k;
8450 my $l;
8451 my $pass;
8452
8453 $l = "no termios; password WILL";
8454 if ($termios) {
8455 $termios->getattr(fileno($stdin));
8456 $k = $termios->getlflag;
8457 $termios->setlflag($k ^ &POSIX::ECHO);
8458 $termios->setattr(fileno($stdin));
8459 $l = "password WILL NOT";
8460 }
8461 print $stdout "enter password for $whoami ($l be echoed): ";
8462 chomp($pass = <$stdin>);
8463 if ($termios) {
8464 print $stdout "\n";
8465 $termios->setlflag($k);
8466 $termios->setattr(fileno($stdin));
8467 }
8468 return $pass;
8469 }
8470
8471 # this returns an immutable token corresponding to the current authenticated
8472 # session. in the case of Basic Auth, it is simply the user:password pair.
8473 # it does not handle OAuth -- that is run by a separate wizard.
8474 # the function then returns (token,secret) which for Basic Auth is token,undef.
8475 # most of the time we will be using tokens in a keyfile, however, so this
8476 # function runs in that case as a stub.
8477 sub authtoken {
8478 my @foo;
8479 my $pass;
8480 my $sig;
8481 my $return;
8482 my $tries = ($hold > 3) ? $hold : 3;
8483 # give up on token if we don't get one
8484
8485 return (undef,undef) if ($anonymous);
8486 return ($tokenkey,$tokensecret)
8487 if (length($tokenkey) && length($tokensecret));
8488 @foo = split(/:/, $user, 2);
8489 $whoami = $foo[0];
8490 die("choose -user=username[:password], or -anonymous.\n")
8491 if (!length($whoami) || $whoami eq '1');
8492 $pass = length($foo[1]) ? $foo[1] : &$getpassword;
8493 die("a password must be specified.\n") if (!length($pass));
8494 return ($whoami, $pass);
8495 }
8496
8497 # this is a sucky nonce generator. I was looking for an awesome nonce
8498 # generator, and then I realized it would only be used once, so who cares?
8499 # *rimshot*
8500 sub generate_nonce { unpack("H9000", pack("u", rand($$).$$.time())); }
8501
8502 # this signs a request with the token and token secret. the result is undef if
8503 # Basic Auth. payload should already be URL encoded and *sorted*.
8504 # this is typically called by stringify_args to get authentication information.
8505 sub signrequest {
8506
8507 # this horrible kludge is needed to account for both 5.005, or for
8508 # 5.6+ installs with no stdlibs and just a bare Perl, both of which
8509 # we support. I hope Larry Wall will forgive me for messing with
8510 # compiler internals next time I see him at church.
8511 BEGIN { $^H |= 0x00000008 unless ($] < 5.006); }
8512
8513 my $resource = shift;
8514 my $payload = shift;
8515
8516 # when we sign the initial request for an token, we obviously
8517 # don't have one yet, so mytoken/mytokensecret can be null.
8518
8519 my $nonce = &generate_nonce;
8520 my @keybytes;
8521 my $sig_base;
8522 my $timestamp = time();
8523 return undef if ($authtype eq 'basic');
8524
8525 # stub for oAuth 2.0
8526 return undef if (!length($oauthkey) || !length($oauthsecret));
8527
8528 (@keybytes) = map { ord($_) }
8529 split(//, $oauthsecret.'&'.$mytokensecret);
8530 if (ref($resource) eq 'ARRAY' || length($payload)) {
8531 # split into _a and _b payloads lexically
8532 my $payload_a = '';
8533 my $payload_b = '';
8534 my $payload_c = ''; # this is for a special case
8535 my $w;
8536 my $aorb = 0;
8537 my $verifier = '';
8538 my $method = "GET";
8539 my $url;
8540
8541 if (length($payload)) {
8542 $method = "POST";
8543 # this is a bit problematic since it won't be
8544 # sorted. we'll deal with this as we need to.
8545 if (ref($resource) eq 'ARRAY') {
8546 $url = &url_oauth_sub($resource->[0]);
8547 $payload .= "&" . $resource->[1];
8548 } else {
8549 $url = &url_oauth_sub($resource);
8550 }
8551 } elsif (ref($resource) eq 'ARRAY') {
8552 $url = &url_oauth_sub($resource->[0]);
8553 $payload = $resource->[1];
8554 } else {
8555 $url = &url_oauth_sub($resource);
8556 }
8557
8558 # this is pretty simplistic but it's really all we need.
8559 # the exception is oauth_verifier: that has to be wormed
8560 # into the middle, and we assume it's just that.
8561 if ($payload !~ /^oauth_verifier/) {
8562 foreach $w (split(/\&/, $payload)) {
8563 $aorb = 1 if
8564 ($w =~ /^[p-z]/ || $w =~ /^o[b-z]/);
8565 $w = &url_oauth_sub("${w}&");
8566 if ($aorb) {
8567 $payload_b .= $w;
8568 } else {
8569 $payload_a .= $w;
8570 }
8571 }
8572 } else {
8573 $payload_c = &url_oauth_sub($payload) . "%26";
8574 $payload_a = $payload_b = '';
8575 $payload =~ s/^oauth_verifier=//;
8576 $verifier = ' oauth_verifier=\\"' . $payload . '\\",';
8577 }
8578 $payload_b =~ s/%26$//;
8579 $sig_base = $method . "&" .
8580 $url . "&" .
8581 (length($payload_a) ? $payload_a : '').
8582 "oauth_consumer_key%3D" . $oauthkey . "%26" .
8583 "oauth_nonce%3D" . $nonce . "%26" .
8584 "oauth_signature_method%3DHMAC-SHA1%26" .
8585 "oauth_timestamp%3D" . $timestamp . "%26" .
8586 (length($mytoken) ?
8587 ("oauth_token%3D" . $mytoken . "%26") : '') .
8588 $payload_c .
8589 "oauth_version%3D1.0" .
8590 (length($payload_b) ? ("%26" . $payload_b) : '');
8591 } else {
8592 $sig_base = "GET&" .
8593 &url_oauth_sub($resource) . "&" .
8594 "oauth_consumer_key%3D" . $oauthkey . "%26" .
8595 "oauth_nonce%3D" . $nonce . "%26" .
8596 "oauth_signature_method%3DHMAC-SHA1%26" .
8597 "oauth_timestamp%3D" . $timestamp . "%26" .
8598 (length($mytoken) ?
8599 ("oauth_token%3D" . $mytoken . "%26") : '') .
8600 $payload_c . # could be part of it
8601 "oauth_version%3D1.0" ;
8602 }
8603 print $stdout
8604 "token-secret: $mytokensecret\nconsumer-secret: $oauthsecret\nsig-base: $sig_base\n"
8605 if ($superverbose);
8606 return ($timestamp, $nonce,
8607 &url_oauth_sub(&hmac_sha1($sig_base, @keybytes)),
8608 $verifier);
8609 }
8610
8611 # this takes a token request and "tries hard" to get it.
8612 sub tryhardfortoken {
8613 my $url = shift;
8614 my $body = shift;
8615 my $tries = shift;
8616 my $rawtoken;
8617 $tries ||= 3;
8618
8619 while($tries) {
8620 my $i;
8621 $rawtoken = &backticks($baseagent, '/dev/null', undef,
8622 $url, $body, 0, @wend);
8623 print $stdout ("token = $rawtoken\n")
8624 if ($superverbose);
8625 my (@keyarr) = split(/\&/, $rawtoken);
8626 my $got_token = '';
8627 my $got_secret = '';
8628 foreach $i (@keyarr) {
8629 my $key;
8630 my $value;
8631
8632 ($key, $value) = split(/\=/, $i);
8633 $got_token = $value if ($key eq 'oauth_token');
8634 $got_secret = $value if ($key eq 'oauth_token_secret');
8635 }
8636 if (length($got_token) && length($got_secret)) {
8637 print $stdout " SUCCEEDED!\n";
8638 return ($got_token, $got_secret);
8639 }
8640 print $stdout ".";
8641 $tries--;
8642 }
8643 print $stdout " FAILED!: \"$rawtoken\"\n";
8644 die("unable to fetch token. here are some possible reasons:\n".
8645 " - root certificates are not updated (see documentation)\n".
8646 " - you entered your authentication information wrong\n".
8647 " - your computer's clock is not set correctly\n" .
8648 " - Twitter farted\n" .
8649 "fix these possible problems, or try again later.\n");
8650 exit;
8651 }
0 oysttyer;2.9.1;;;https://raw.githubusercontent.com/oysttyer/oysttyer/2.9.1/oysttyer.pl;
1 --__--
2 --__--
3 --__--