· 7 years ago · Nov 22, 2017, 10:24 PM
1#
2# twittybort
3#
4
5package require htmlparse
6package require http
7package require inifile
8package require twitoauth
9package require twitlib
10
11namespace eval ::twitter {
12 # Check for tweets every 1, 5, or 10 min.
13 #
14 # This option must be 1, 5, or 10! You can't have anything else right now.
15 #
16 # Note using 1 minute may put you up against Twitter's API limits if you have
17 # both home and mentions polling enabled.
18 variable update_time 5
19
20 # Show tweet number in tweets shown in channels (1 = on, 0 = off).
21 # This is only really relevant if you are going to !retweet.
22 variable show_tweetid 0
23
24 # Path to the configuration file. The path is relative to the Eggdrop root
25 # directory. You can specify an absolute path.
26 #
27 # The configuration file currently contains a small number of the available
28 # options. Eventually we may move all options to it.
29 variable config_file twitter.conf
30
31 # Control what we poll. Each of these poll types is a separate API request
32 # every 'update_time' interval, so if you don't need/want one, then it is
33 # more efficient to disable it.
34 #
35 # By default we poll only the home timeline. This means you will see tweets
36 # of users you follow.
37
38 # Whether to poll home timeline.
39 variable poll_home_timeline 1
40 # Whether to poll mentions timeline.
41 variable poll_mentions_timeline 0
42
43 # Maximum characters per output line.
44 variable line_length 400
45
46 #
47 # You shouldn't need to change anything below this point!
48 #
49
50 # Number of followers to output when listing followers. The API can return a
51 # maximum of 200 at a time but you probably don't want to spam channels with
52 # that many! If we wanted to see all of them then it is possible to make
53 # multiple API calls using the cursor parameter to page through.
54 variable followers_limit 50
55
56 # This file holds state information (id of last seen tweet, oauth keys).
57 variable state_file "scripts/twitter.state"
58
59 variable output_cmd putserv
60
61 variable last_update
62 variable last_msg
63
64 # Map of accounts you follow to the channels to show statuses in.
65 #
66 # If an account isn't in this map then its statuses go to all +twitter
67 # channels.
68 #
69 # Define this mapping in the config file.
70 variable account_to_channels [dict create]
71
72 # Channel command binds.
73 bind pub -|- "!twit" ::twitter::tweet
74 bind pub -|- "!tweet" ::twitter::tweet
75 bind pub -|- "!twit_msg" ::twitter::msg
76 bind pub -|- "!twit_trends" ::twitter::trends_global
77 bind pub o|o "!follow" ::twitter::follow
78 bind pub o|o "!unfollow" ::twitter::unfollow
79 bind pub -|- "!twit_updates" ::twitter::updates
80 bind pub -|- "!twit_msgs" ::twitter::msgs
81 bind pub -|- "!twit_search" ::twitter::search
82 bind pub -|- "!twit_searchusers" ::twitter::search_users
83 bind pub -|- "!twit_get_tweet" ::twitter::get_tweet
84
85 variable followers_trigger !followers
86 bind pub -|- $followers_trigger ::twitter::followers
87
88 variable following_trigger !following
89 bind pub -|- $following_trigger ::twitter::following
90
91 bind pub -|- "!retweet" ::twitter::retweet
92 bind pub o|o "!update_interval" ::twitter::update_interval
93
94 bind pub -|- !twitstatus ::twitter::status
95
96 # OAuth channel command binds.
97 bind pub o|o "!twit_request_token" ::twitter::oauth_request
98 bind pub o|o "!twit_access_token" ::twitter::oauth_access
99
100 # Save our state on save event.
101 bind evnt -|- "save" ::twitter::write_states
102
103 bind dcc -|- twitter-status ::twitter::dcc_status
104
105 # Add channel flag +/-twitter.
106 setudef flag twitter
107}
108
109# Handle retrieval of OAuth request token
110proc ::twitter::oauth_request {nick uhost hand chan argv} {
111 if {![channel get $chan twitter]} { return }
112 set argv [split $argv]
113 if {[llength $argv] != 2} {
114 $::twitter::output_cmd "PRIVMSG $chan :Usage: !twit_request_token <consumer key> <consumer secret>"
115 return
116 }
117 lassign $argv ::twitlib::oauth_consumer_key ::twitlib::oauth_consumer_secret
118
119 if {[catch {::twitoauth::get_request_token $::twitlib::oauth_consumer_key $::twitlib::oauth_consumer_secret} data]} {
120 $::twitter::output_cmd "PRIVMSG $chan :Error: $data"
121 return
122 }
123
124 set url [dict get $data auth_url]
125 $::twitter::output_cmd "PRIVMSG $chan :To get your authentication verifier, visit ${url} and allow the application on your Twitter account."
126 $::twitter::output_cmd "PRIVMSG $chan :Then call !twit_access_token [dict get $data oauth_token] [dict get $data oauth_token_secret] <PIN from authorization URL of !twit_request_token>"
127}
128
129# Handle retrieval of OAuth access token
130# if success, we store $::twitlib::oauth_token and $::twitlib::oauth_token_secret
131proc ::twitter::oauth_access {nick uhost hand chan argv} {
132 if {![channel get $chan twitter]} { return }
133
134 set args [split $argv]
135 if {[llength $args] != 3} {
136 $::twitter::output_cmd "PRIVMSG $chan :Usage: !twit_access_token <oauth_token> <oauth_token_secret> <PIN> (get these from !twit_request_token)"
137 return
138 }
139 lassign $args oauth_token oauth_token_secret pin
140
141 if {[catch {::twitoauth::get_access_token $::twitlib::oauth_consumer_key $::twitlib::oauth_consumer_secret $oauth_token $oauth_token_secret $pin} data]} {
142 $::twitter::output_cmd "PRIVMSG $chan :Error: $data"
143 return
144 }
145
146 # reset stored state
147 set ::twitlib::last_id 1
148 set ::twitlib::last_mentions_id 1
149 set ::twitter::last_update 1
150 set ::twitter::last_msg 1
151
152 set ::twitlib::oauth_token [dict get $data oauth_token]
153 set ::twitlib::oauth_token_secret [dict get $data oauth_token_secret]
154 set screen_name [dict get $data screen_name]
155 $::twitter::output_cmd "PRIVMSG $chan :Successfully retrieved access token for \002${screen_name}\002."
156}
157
158# set the update time by recreating the time bind.
159proc ::twitter::set_update_time {delay} {
160 if {$delay != 1 && $delay != 10 && $delay != 5} {
161 set delay 10
162 }
163 ::twitter::flush_update_binds
164 if {$delay == 1} {
165 bind time - "* * * * *" ::twitter::update
166 return
167 }
168 # NOTE: */x cron syntax is not supported by eggdrop.
169 if {$delay == 5} {
170 bind time - "?0 * * * *" ::twitter::update
171 bind time - "?5 * * * *" ::twitter::update
172 return
173 }
174 # 10
175 bind time - "?0 * * * *" ::twitter::update
176}
177
178# remove our time bind.
179proc ::twitter::flush_update_binds {} {
180 foreach binding [binds time] {
181 if {[lindex $binding 4] == "::twitter::update"} {
182 unbind [lindex $binding 0] [lindex $binding 1] [lindex $binding 2] \
183 [lindex $binding 4]
184 }
185 }
186}
187
188# Change time between automatic update fetches
189proc ::twitter::update_interval {nick uhost hand chan argv} {
190 if {![channel get $chan twitter]} { return }
191
192 if {$argv != 1 && $argv != 10 && $argv != 5} {
193 $::twitter::output_cmd "PRIVMSG $chan :Usage: !update_interval <1, 5, or 10>"
194 return
195 }
196
197 ::twitter::set_update_time $argv
198
199 $::twitter::output_cmd "PRIVMSG $chan :Update interval set to $argv minute(s)."
200}
201
202proc ::twitter::status {nick uhost hand chan argv} {
203 set screen_name [::twitlib::get_my_screen_name]
204 $::twitter::output_cmd "PRIVMSG $chan :I'm @$screen_name."
205}
206
207# Output decoded/split string to given channel
208proc ::twitter::output {chan str} {
209 set str [::htmlparse::mapEscapes $str]
210 set str [regsub -all -- {\n} $str " "]
211 $::twitter::output_cmd "PRIVMSG $chan :$str"
212}
213
214# Format status update and output it to the channel.
215proc ::twitter::output_update {chan name id str} {
216 set out "\002$name\002: $str"
217 if {$::twitter::show_tweetid} {
218 append out " ($id)"
219 }
220 ::twitter::output $chan $out
221}
222
223# Retweet given id
224proc ::twitter::retweet {nick uhost hand chan argv} {
225 if {![channel get $chan twitter]} { return }
226
227 if {[string length $argv] < 1 || ![regexp {^\d+$} $argv]} {
228 $::twitter::output_cmd "PRIVMSG $chan :Usage: !retweet <id>"
229 return
230 }
231
232 # Setup url since id is not given as params for some reason...
233 set url "${::twitlib::retweet_url}${argv}.json"
234
235 if {[catch {::twitlib::query $url {} POST} result]} {
236 $::twitter::output_cmd "PRIVMSG $chan :Retweet failure. ($argv) (You can't retweet your own updates!)"
237 return
238 }
239
240 $::twitter::output_cmd "PRIVMSG $chan :Retweet sent. ( http://twitter.com/twittybort )"
241}
242
243# Follow a user (by screen name)
244proc ::twitter::follow {nick uhost hand chan argv} {
245 if {![channel get $chan twitter]} { return }
246
247 set argv [string trim $argv]
248 set args [split $argv " "]
249 if {[llength $args] == 0} {
250 ::twitter::follow_usage $chan
251 return
252 }
253 set screen_name [string tolower [lindex $args 0]]
254
255 set channels [list]
256 if {[llength $args] > 1} {
257 foreach channel [lrange $args 1 end] {
258 set channel [string tolower $channel]
259
260 if {[string index $channel 0] != "#"} {
261 ::twitter::follow_usage $chan
262 return
263 }
264
265 if {[lsearch -exact $channels $channel] != -1} {
266 continue
267 }
268
269 lappend channels $channel
270 }
271 }
272
273 set query [list screen_name $screen_name]
274 if {[catch {::twitlib::query $::twitlib::follow_url $query} result]} {
275 $::twitter::output_cmd "PRIVMSG $chan :Unable to follow or already friends with $screen_name!"
276 putlog "Unable to follow or already friends with $screen_name: $result"
277 return
278 }
279
280 if {[dict exists $result error]} {
281 ::twitter::output $chan "Follow failed ($screen_name): [dict get $result error]"
282 return
283 }
284
285 ::twitter::output $chan "Now following [dict get $result screen_name]! \002http://twitter.com/$screen_name\002 - ( http://twitter.com/twittybort )"
286
287 # Update mappings and save config no matter what (even if there is no
288 # mapping). If they specified no channels then this lets us reset mapping to
289 # all channels if the account was previously mapped.
290
291 if {[llength $channels] == 0} {
292 if {[dict exists $::twitter::account_to_channels $screen_name]} {
293 dict unset ::twitter::account_to_channels $screen_name
294 }
295 } else {
296 dict set ::twitter::account_to_channels $screen_name $channels
297 }
298
299 ::twitter::save_config
300}
301
302proc ::twitter::follow_usage {chan} {
303 $::twitter::output_cmd "PRIVMSG $chan :Usage: !follow <screen name> \[#channel1 #channel2 ...\]"
304 $::twitter::output_cmd "PRIVMSG $chan :If you specify channel(s) then the screen name's statuses will only show in those channels. This updates the config. To show them in all channels, do not specify any here."
305}
306
307# Unfollow a user (by screen name)
308proc ::twitter::unfollow {nick uhost hand chan argv} {
309 if {![channel get $chan twitter]} { return }
310
311 if {[string length $argv] < 1} {
312 $::twitter::output_cmd "PRIVMSG $chan :Usage: !unfollow <screen name>"
313 return
314 }
315
316 if {[catch {::twitlib::query $::twitlib::unfollow_url [list screen_name $argv]} result]} {
317 $::twitter::output_cmd "PRIVMSG $chan :Unfollow failed. ($argv)"
318 return
319 }
320
321 if {[dict exists $result error]} {
322 ::twitter::output $chan "Unfollow failed ($argv): [dict get $result error]"
323 return
324 }
325
326 ::twitter::output $chan "Unfollowed [dict get $result screen_name]."
327}
328
329# Get last n, n [1, 20] updates
330proc ::twitter::updates {nick uhost hand chan argv} {
331 if {![channel get $chan twitter]} { return }
332
333 if {[string length $argv] < 1 || ![string is integer $argv] || $argv > 20 || $argv < 1} {
334 $::twitter::output_cmd "PRIVMSG $chan :Usage: !twit_updates <#1 to 20>"
335 return
336 }
337
338 set params [list \
339 count $argv \
340 tweet_mode extended \
341 ]
342
343 if {[catch {::twitlib::query $::twitlib::home_url $params GET} result]} {
344 $::twitter::output_cmd "PRIVMSG $chan :Retrieval error: $result."
345 return
346 }
347
348 if {[llength $result] == 0} {
349 $::twitter::output_cmd "PRIVMSG $chan :No updates. ( http://twitter.com/twittybort )"
350 return
351 }
352
353 set result [::twitlib::fix_statuses $result]
354
355 set result [lreverse $result]
356 foreach status $result {
357 dict with status {
358 ::twitter::output_update $chan [dict get $user screen_name] $id $full_text
359 }
360 }
361}
362
363# Return top 5 results for query $argv
364proc ::twitter::search {nick uhost hand chan argv} {
365 # Let this command work in any channel we're in.
366
367 if {[string length $argv] < 1 || [string length $argv] > 140} {
368 $::twitter::output_cmd "PRIVMSG $chan :Usage: !twit_search <string 140 chars or less>"
369 return
370 }
371
372 set params [list \
373 q $argv \
374 count 4 \
375 tweet_mode extended \
376 ]
377
378 if {[catch {::twitlib::query $::twitlib::search_url $params GET} data]} {
379 $::twitter::output_cmd "PRIVMSG $chan :Search error ($data)"
380 return
381 }
382
383 if {[dict exists $data error]} {
384 ::twitter::output $chan "Search failed ($argv): [dict get $result error]"
385 return
386 }
387
388 set statuses [dict get $data statuses]
389 set statuses [::twitlib::fix_statuses $statuses]
390 set count 0
391 foreach status $statuses {
392 set user [dict get $status user]
393 ::twitter::output $chan "\002[dict get $user screen_name]\002: [dict get $status full_text]"
394 }
395}
396
397# Get first 5 results from users search
398proc ::twitter::search_users {nick uhost hand chan argv} {
399 # Let this command work in any channel we're in.
400
401 if {[string length $argv] < 1} {
402 $::twitter::output_cmd "PRIVMSG $chan :Usage: !twit_searchusers <string>"
403 return
404 }
405
406 if {[catch {::twitlib::query $::twitlib::search_users_url [list q $argv per_page 5] GET} data]} {
407 $::twitter::output_cmd "PRIVMSG $chan :Search error ($data)."
408 return
409 }
410
411 foreach result $data {
412 ::twitter::output $chan "#[incr count] \002[dict get $result screen_name]\002 Name: [dict get $result name] Location: [dict get $result location] Description: [dict get $result description]"
413 }
414}
415
416proc ::twitter::get_tweet {nick uhost hand chan argv} {
417 set id [string trim $argv]
418 if {$id == ""} {
419 $::twitter::output_cmd "PRIVMSG $chan :Usage: !twit_get_tweet <ID>"
420 return
421 }
422
423 if {[catch {::twitlib::get_status_by_id $id} status]} {
424 $::twitter::output_cmd "PRIVMSG $chan :Error: $status"
425 return
426 }
427
428 ::twitter::output_update $chan [dict get $status user screen_name] $id \
429 [dict get $status full_text]
430}
431
432# Look up and output the users following an account (the most recent).
433#
434# If no account is given, we output who is following us.
435#
436# We output at most $::twitter::followers_limit screen names.
437proc ::twitter::followers {nick uhost hand chan argv} {
438 if {![channel get $chan twitter]} { return }
439
440 set argv [string trim $argv]
441 set args [split $argv " "]
442 if {[llength $args] > 1} {
443 ::twitter::output $chan "Usage: $::twitter::followers_trigger \[screen name\] (defaults to me)"
444 return
445 }
446 set screen_name {}
447 if {[llength $args] == 1} {
448 set screen_name [lindex $args 0]
449 }
450
451 set query_list [list count $::twitter::followers_limit]
452 if {$screen_name != ""} {
453 lappend query_list screen_name $screen_name
454 }
455 if {[catch {::twitlib::query $::twitlib::followers_url $query_list GET} \
456 result]} {
457 ::twitter::output $chan "Error fetching followers."
458 putlog "Error fetching followers: $result"
459 return
460 }
461
462 # Sort: First following -> last following.
463 set users [lreverse [dict get $result users]]
464
465 # Format.
466 set followers []
467 foreach user $users {
468 append followers "[dict get $user screen_name] "
469 }
470 set followers [string trim $followers]
471
472 foreach line [::twitter::split_line $::twitter::line_length $followers] {
473 if {$screen_name == ""} {
474 ::twitter::output $chan "I have followers: $followers"
475 } else {
476 ::twitter::output $chan "$screen_name has followers: $followers"
477 }
478 }
479
480 if {[llength $users] == 0} {
481 if {$screen_name == ""} {
482 ::twitter::output $chan "I have no followers."
483 } else {
484 ::twitter::output $chan "$screen_name has no followers."
485 }
486 }
487}
488
489# Look up and output the users an account is following (the most recent).
490#
491# If no account is given, we output who we are following.
492#
493# We output at most $::twitter::followers_limit screen names.
494proc ::twitter::following {nick uhost hand chan argv} {
495 if {![channel get $chan twitter]} { return }
496
497 set argv [string trim $argv]
498 set args [split $argv " "]
499 if {[llength $args] > 1} {
500 ::twitter::output $chan "Usage: $::twitter::following_trigger \[screen name\] (defaults to me)"
501 return
502 }
503 set screen_name {}
504 if {[llength $args] == 1} {
505 set screen_name [lindex $args 0]
506 }
507
508 set query_list [list count $::twitter::followers_limit]
509 if {$screen_name != ""} {
510 lappend query_list screen_name $screen_name
511 }
512 if {[catch {::twitlib::query $::twitlib::following_url $query_list GET} \
513 result]} {
514 ::twitter::output $chan "Error looking Twitter friends."
515 putlog "Error looking up Twitter friends: $result"
516 return
517 }
518
519 # Sort: First following -> last following.
520 set users [lreverse [dict get $result users]]
521
522 # Format output.
523 set following ""
524 foreach user $users {
525 append following "[dict get $user screen_name] "
526 }
527 set following [string trim $following]
528
529 foreach line [::twitter::split_line $::twitter::line_length $following] {
530 if {$screen_name == ""} {
531 ::twitter::output $chan "I'm following: $line"
532 } else {
533 ::twitter::output $chan "$screen_name is following: $line"
534 }
535 }
536
537 if {[llength $users] == 0} {
538 if {$screen_name == ""} {
539 ::twitter::output $chan "I'm not following anyone."
540 } else {
541 ::twitter::output $chan "$screen_name is not following anyone."
542 }
543 }
544}
545
546# Retrieve and output global trends.
547proc ::twitter::trends_global {nick uhost hand chan argv} {
548 # Let this command work in any channel we're in.
549
550 # id is a WOED (where on earth id). 1 means global.
551 if {[catch {::twitlib::query $::twitlib::trends_place_url [list id 1] GET} result]} {
552 $::twitter::output_cmd "PRIVMSG $chan :Trends request failed: $result."
553 return
554 }
555
556 # We receive an array with one element - the object with our result.
557 #
558 # The object has keys trends (list of trends), as_of, created_at, and
559 # locations.
560 set result [lindex $result 0]
561
562 # Pull out the trends object. This is an array of JSON objects (as dicts).
563 # What I care about in these objects is the name. This is the trend name.
564 set trends [dict get $result trends]
565
566 set output ""
567 set count 0
568 foreach trend $trends {
569 if {$output != ""} {
570 append output " "
571 }
572
573 append output "\002#[incr count]\002 [dict get $trend name]"
574
575 if {$count >= 20} {
576 break
577 }
578 }
579
580 foreach line [::twitter::split_line $::twitter::line_length $output] {
581 ::twitter::output $chan $line
582 }
583}
584
585# Direct messages
586# Get last n, n [1, 20] messages or new if no argument
587proc ::twitter::msgs {nick uhost hand chan argv} {
588 if {![channel get $chan twitter]} { return }
589
590 if {[string length $argv] == 1 && [string is integer $argv] && $argv < 20} {
591 set params [list count $argv]
592 } else {
593 set params [list since_id $::twitter::last_msg]
594 }
595
596 if {[catch {::twitlib::query $::twitlib::msgs_url $params GET} result]} {
597 $::twitter::output_cmd "PRIVMSG $chan :Messages retrieval failed."
598 return
599 }
600
601 if {[llength $result] == 0} {
602 $::twitter::output_cmd "PRIVMSG $chan :No new messages."
603 return
604 }
605
606 foreach msg $result {
607 dict with msg {
608 if {$id > $::twitter::last_msg} {
609 set ::twitter::last_msg $id
610 }
611 ::twitter::output $chan "\002From\002 $sender_screen_name: $text ($created_at)"
612 }
613 }
614}
615
616# Send direct message to a user
617# TODO: replace lrange by a substring starting at the first space to fix
618# 'unmatched open brace in list' issue if the tweet contains one { not balanced
619# by a }.
620proc ::twitter::msg {nick uhost hand chan argv} {
621 if {![channel get $chan twitter]} { return }
622 set argv [split $argv]
623
624 if {[llength $argv] < 2 || [string length [join [lrange $argv 1 end]]] > 140} {
625 $::twitter::output_cmd "PRIVMSG $chan :Usage: !twit_msg <username> <msg 140 chars or less>"
626 return
627 }
628
629 set name [lindex $argv 0]
630 set msg [lrange $argv 1 end]
631 set l [list screen_name $name text $msg]
632
633 if {[catch {::twitlib::query $::twitlib::msg_url $l} data]} {
634 $::twitter::output_cmd "PRIVMSG $chan :Message to \002$name\002 failed ($data)! (Are they following you?)"
635 } else {
636 ::twitter::output $chan "Message sent."
637 }
638}
639
640# Send status update (tweet)
641proc ::twitter::tweet {nick uhost hand chan argv} {
642 if {![channel get $chan twitter]} { return }
643
644 if {[string length $argv] > 140 || $argv == ""} {
645 $::twitter::output_cmd "PRIVMSG $chan :Usage: !tweet <up to 140 characters>"
646 return
647 }
648
649 if {[catch {::twitlib::query $::twitlib::status_url [list status $argv]} result]} {
650 $::twitter::output_cmd "PRIVMSG $chan :Tweet failed! ($argv) Error: $result."
651 return
652 }
653
654 set update_id [dict get $result id]
655 if {$update_id == $::twitter::last_update} {
656 $::twitter::output_cmd "PRIVMSG $chan :Tweet failed: Duplicate of tweet #$update_id. ($argv)"
657 return
658 }
659 set ::twitter::last_update $update_id
660
661 ::twitter::output $chan "Tweet sent. ( http://twitter.com/twittybort )"
662}
663
664# send timeline updates to all +twitter channels.
665proc ::twitter::output_updates {updates} {
666 # Track what channels we output each status to. This is mainly useful for
667 # testing so we can examine the return value for where an update was sent.
668 set id_to_channels [dict create]
669
670 set all_channels [channels]
671
672 foreach status $updates {
673 # Figure out what channels to output the status to.
674 #
675 # By default we output to all +twitter channels.
676 #
677 # However, if the account is mapped to particular channels, then output only
678 # to those. Note they must be +twitter as well.
679
680 set account [dict get $status screen_name]
681 set account [string trim $account]
682 set account [string tolower $account]
683 if {$account == ""} {
684 continue
685 }
686
687 set account_channels [list]
688 if {[dict exists $::twitter::account_to_channels $account]} {
689 set account_channels [dict get $::twitter::account_to_channels $account]
690 }
691
692 set output_channels $all_channels
693 if {[llength $account_channels] > 0} {
694 set output_channels $account_channels
695 }
696
697 foreach ch $output_channels {
698 if {[lsearch -exact -nocase $all_channels $ch] == -1} {
699 continue
700 }
701 if {![channel get $ch twitter]} {
702 continue
703 }
704
705 set id [dict get $status id]
706 # Don't use $account here. We've done things like lowercase it.
707 ::twitter::output_update $ch [dict get $status screen_name] $id \
708 [dict get $status full_text]
709
710 if {![dict exists $id_to_channels $id]} {
711 dict set id_to_channels $id [list]
712 }
713 dict lappend id_to_channels $id $ch
714 }
715 }
716
717 return $id_to_channels
718}
719
720# grab unseen status updates and output them to +twitter channels.
721proc ::twitter::update {min hour day month year} {
722 # home timeline updates.
723 if {$::twitter::poll_home_timeline} {
724 if {[catch {::twitlib::get_unseen_updates} updates]} {
725 putlog "Update retrieval (home) failed: $updates"
726 return
727 }
728 ::twitter::output_updates $updates
729 }
730
731 # mentions timeline updates.
732 if {$::twitter::poll_mentions_timeline} {
733 if {[catch {::twitlib::get_unseen_mentions} updates]} {
734 putlog "Update retrieval (mentions) failed: $updates"
735 return
736 }
737 ::twitter::output_updates $updates
738 }
739}
740
741# Get saved ids/state
742proc ::twitter::get_states {} {
743 if {[catch {open $::twitter::state_file r} fid]} {
744 set ::twitlib::last_id 1
745 set ::twitter::last_update 1
746 set ::twitter::last_msg 1
747 set ::twitlib::last_mentions_id 1
748 return
749 }
750
751 set data [read -nonewline $fid]
752 set states [split $data \n]
753 close $fid
754
755 set ::twitlib::last_id [lindex $states 0]
756 set ::twitter::last_update [lindex $states 1]
757 set ::twitter::last_msg [lindex $states 2]
758 set ::twitlib::oauth_token [lindex $states 3]
759 set ::twitlib::oauth_token_secret [lindex $states 4]
760 set ::twitlib::oauth_consumer_key [lindex $states 5]
761 set ::twitlib::oauth_consumer_secret [lindex $states 6]
762
763 set ::twitlib::last_mentions_id 1
764 if {[llength $states] >= 8} {
765 set ::twitlib::last_mentions_id [lindex $states 7]
766 }
767}
768
769# Save states to file
770proc ::twitter::write_states {args} {
771 set fid [open $::twitter::state_file w]
772 puts $fid $::twitlib::last_id
773 puts $fid $::twitter::last_update
774 puts $fid $::twitter::last_msg
775 puts $fid $::twitlib::oauth_token
776 puts $fid $::twitlib::oauth_token_secret
777 puts $fid $::twitlib::oauth_consumer_key
778 puts $fid $::twitlib::oauth_consumer_secret
779 puts $fid $::twitlib::last_mentions_id
780 close $fid
781}
782
783proc ::twitter::load_config {} {
784 set ::twitter::account_to_channels [dict create]
785
786 if {![file exists $::twitter::config_file]} {
787 putlog "twitter.tcl: Config file $::twitter::config_file does not exist, skipping"
788 return
789 }
790
791 if {[catch {::ini::open $::twitter::config_file r} ini]} {
792 putlog "twitter.tcl: Error opening configuration file: $::twitter::config_file: $ini"
793 return
794 }
795
796 set mapping_section account-to-channel-mapping
797 if {![::ini::exists $ini $mapping_section]} {
798 ::ini::close $ini
799 return
800 }
801
802 foreach key [::ini::keys $ini $mapping_section] {
803 set account [string trim $key]
804 if {[string length $account] == 0} {
805 continue
806 }
807 set account [string tolower $account]
808
809 # The ini is key/value. If you list the same key multiple times we get the
810 # first definition's value multiple times, so it is not useful and is
811 # probably not what you intended.
812 if {![dict exists $::twitter::account_to_channels $account]} {
813 dict set ::twitter::account_to_channels $account [list]
814 } else {
815 putlog "twitter.tcl: Error: $account is in $mapping_section twice"
816 continue
817 }
818
819 set channels_string [::ini::value $ini $mapping_section $key]
820 set channels [split $channels_string ,]
821 foreach channel $channels {
822 set channel [string trim $channel]
823 if {[string length $channel] == 0} {
824 continue
825 }
826 set channel [string tolower $channel]
827
828 dict lappend ::twitter::account_to_channels $account $channel
829 }
830 }
831
832 ::ini::close $ini
833}
834
835proc ::twitter::save_config {} {
836 # r+ is read/write
837 if {[catch {::ini::open $::twitter::config_file r+} ini]} {
838 putlog "twitter.tcl: Error opening configuration file: $::twitter::config_file: $ini"
839 return
840 }
841
842 set mapping_section account-to-channel-mapping
843
844 # Clear out the current mappings. Note that comments in the section do not
845 # reliably stick around. They seem to stick around if the comment is above an
846 # account that we have after rewriting the file. But if the comment is above a
847 # key that we lose a mapping for all together then we lose the comment as
848 # well.
849 if {[::ini::exists $ini $mapping_section]} {
850 foreach key [ini::keys $ini $mapping_section] {
851 ::ini::delete $ini $mapping_section $key
852 }
853 }
854
855 set account_count 0
856 foreach account [dict keys $::twitter::account_to_channels] {
857 set channels [dict get $::twitter::account_to_channels $account]
858 if {[llength $channels] == 0} {
859 continue
860 }
861 set channels_csv [join $channels ,]
862 ::ini::set $ini $mapping_section $account $channels_csv
863 incr account_count
864 }
865
866 ::ini::commit $ini
867 ::ini::close $ini
868 putlog "twitter.tcl: Wrote $::twitter::config_file ($account_count accounts)"
869}
870
871proc ::twitter::write_status_to_log {} {
872 putlog "twitter.tcl: Config file is $::twitter::config_file"
873
874 putlog "twitter.tcl: Mapped [dict size $::twitter::account_to_channels] accounts to specific channels"
875 foreach account [dict keys $::twitter::account_to_channels] {
876 set channels {}
877 foreach c [dict get $::twitter::account_to_channels $account] {
878 if {$channels == ""} {
879 append channels $c
880 } else {
881 append channels ", $c"
882 }
883 }
884 set channel_count [llength [dict get $::twitter::account_to_channels $account]]
885 putlog "twitter.tcl: $account shows in $channel_count channels: $channels"
886 }
887}
888
889# Split long line into list of strings for multi line output to irc.
890#
891# Split into strings of ~max.
892proc ::twitter::split_line {max str} {
893 set last [expr {[string length $str] -1}]
894 set start 0
895 set end [expr {$max -1}]
896
897 set lines []
898
899 while {$start <= $last} {
900 if {$last >= $end} {
901 set end [string last { } $str $end]
902 }
903
904 lappend lines [string trim [string range $str $start $end]]
905 set start $end
906 set end [expr {$start + $max}]
907 }
908
909 return $lines
910}
911
912proc ::twitter::dcc_status {handle idx text} {
913 ::twitter::write_status_to_log
914 return 1
915}
916
917::twitter::get_states
918::twitter::load_config
919::twitter::set_update_time $::twitter::update_time
920::twitter::write_status_to_log
921
922putlog "twitter.tcl - twittybort - loaded."