· 5 years ago · Jul 06, 2020, 04:26 AM
1###############################################################################
2# Name: Youtube Title V2
3# Author: Jan Milants <viper@anope.org>
4# Version: 2.1 (22/08/2016)
5# Eggdrop Version: 1.6.x
6# Requires TCL version: 8.5
7# Package dependencies: http, tls, json
8# Credits: Based on original YouTube Script
9# by jotham.read@gmail.com
10# Design inspiration from
11# youtube.tcl by Mookie.
12###############################################################################
13# Description
14# -------------
15# La escritura supervisa canales de texto para relaciones a Youtube.
16# Cuando encontrado, esto preguntará al servidor google para detalles, como el título de vídeo
17# y el número de visiones del vídeo youtube y anuncian los resultados en el canal.
18# La escritura también apoya la busca youtube con la orden, !youtube <texto a buscar>
19# o "!youtube <texto a buscar>". Opcionalmente, el número de resultados de la búsqueda deseados puede ser
20# especificando la utilización"!youtube + <el número de resultados> <buscan el texto>".
21# En la respuesta, la pregunta de búsqueda será pasada al youtube API y el mejor
22# el resultado (s) que está será unido en el canal.
23#
24# Esta escritura será activa en cualquier canal en el cual ella resida con el "youtube" flag.
25# existe dos formas en activar esta tcl una mediante el partylien ejemplo ".chanset #tu-canal +youtube" y la otra
26# forma es activando en el canal abierto ejemplo !youtube on
27# en ambos comanodos solo puede ser activada por el usuario que tenga los flags mno.
28#
29# Esta escritura pone en práctica una protección de antiflood, muy básica para búsquedas de YouTube,
30# requiriendo a usuarios, o mejor dicho todos los clientes con mismo userhost, esperar
31# un intervalo predeterminado antes de que ellos puedan buscar otra vez.
32# Nota que esto no aplica a YouTube URLs pegado en el canal.
33#
34# Getting your own Google API key
35# ---------------------------------
36# This script uses the Google Youtube Data API V3, which requires an API key to
37# authorize access to the API and is used as basis for limiting request per day etc.
38# Instructions can be found on
39# https://developers.google.com/youtube/registering_an_application
40# Required steps in short:
41# 1. Go to the Google developers console https://console.developers.google.com
42# 2. Create a new project. Give it a name like 'eggdrop', doesn't matter much.
43# 3. When the project is loaded, select menu "Library" under "API Manager"
44# This should load the list of available Google APIs on the right.
45# 4. In the list of APIs, select the "YouTube Data API".
46# This should load a page describing the "YouTube Data API v3".
47# 5. Enable the API for you application by clicking "Enable" next to the API name.
48# 6. Select menu "Credentials" and click "Create credentials".
49# 7. Select type "API key" type "server key".
50# 8. Optionally, fill in the IP(s) or IP range from which the eggdrop bot will send
51# requests to google's servers. This is a whitelist, if the request
52# comes from a different IP, it will be rejected.
53# Note: If my-ip or my-hostname is configured in eggdrop.conf, they should
54# be entered here.
55# 9. Click "Create".
56# You now have an API KEY; copy it to the config section below.
57#
58# !!! IMPORTANT !!!
59# When loading this script alongside other scripts which initiate web service calls,
60# ensure this script is loaded last! The script creates a handler for HTTPS connection
61# and sets the source IP to the my-ip or my-hostname from eggdrop.conf.
62# Most other scripts will not correctly enforce the source IP of requests and
63# can overwrite this scripts HTTPS handler. This results in connections coming from another
64# IP on the machine and may thus be rejected by the Google API Servers.
65# The typical error message logged would be "Error processing web service reply".
66#
67###############################################################################
68# Changes:
69# 2.10 22/08/16
70# Added a default API key since IPs don't need to be individually whitelisted anymore.
71# Added support for YouTube searches returning multiple search results.
72# Added basic anti-flood measures by implementing a minimum interval between searches.
73# Updated dependency checking to be non-fatal to the hosting bot. (Suggested by caesar.)
74# Updated documentation for creating your own API key to match new dev console.
75# Fixed the socket not enabling TLS usage on some systems. (Fix suggested by Madalin.)
76# 2.00 28/08/14
77# Started development (Jan).
78# Almost complete rewrite most notable changes:
79# * Use the YouTube Data API V3. (Requires TLS support!)
80# * Strip out flat_json_decoder and use json & dict packages instead.
81# * Strip out tinyurl support. Better to use youtu.be in the response.
82# * Many more data elements supported in response format (possible by new API).
83# * Added possiblity to turn the script on/off on a channel by channel basis.
84# * Added ability to search youtube and return the first result.
85# 0.51 09/30/13
86# Small correction for caps in url (but not video id)
87# 0.5 01/02/09
88# Added better error reporting for restricted youtube content.
89# 0.4 10/11/09
90# Changed title scraping method to use the oembed api.
91# Added crude JSON decoder library.
92# 0.3 02/03/09
93# Fixed entity decoding problems in return titles.
94# Added customisable response format.
95# Fixed rare query string bug.
96###############################################################################
97#
98# Configuration
99#
100###############################################################################
101
102# API key assigned to your Google account.
103# Find detailed instructions for getting your own API key above or use mine.
104set youtube(api_key) "AIzaSyBrzjhGQ2-_A86Xv_Ay8-B8h9-hg5oAhos"
105
106# Base URI for links to YouTube videos.
107# Either use the normal YouTube link or youtu.be for shorter URLs.
108# I'd recommend keeping the HTTPS to avoid exposing user data.
109set youtube(base_url) "https://www.youtube.com/watch?v="
110set youtube(base_url) "https://www.youtu.be/"
111
112# Date/time format
113# The format to be used when showing dates, for example in publish date.
114# All times are in UTC.
115#
116# Available tokens:
117# %year% 4 digit year notation
118# %month% 2 digit month notation
119# %day% 2 digit day of the month notation
120# %hours% 2 digit hour notation on a 24hours basis
121# %minutes% 2 digit minutes notation
122# %seconds% 2 digit seconds notation
123#
124# Example:
125# "%day%/%month%/%year% %hours%:%minutes% UTC"
126set youtube(date_format) "%hours%:%minutes%:%seconds%"
127
128
129# Response Formats
130# Template of the reply to be send to the channel showing the YouTube video details.
131# A separate response can be set for replies to a pasted URL or to a query.
132#
133# Available tokens in the response format:
134# %botnick% Nickname of bot
135# %poster% Nickname of person who posted the YouTube link
136# %youtube_url% URL to the YouTube link (This may not be the exact same
137# URL that was posted since it's rewritten based on the format
138# above to ensure all links posted by the bot are HTTPS.)
139# %id% ID of the linked YouTube video.
140# %author% Author/Uploader/channel of the video.
141# %title% Title of YouTube link
142# %description% Description of the video.
143# (Note that this is generally a VERY long text!)
144# %published% Date & time the video was published.
145# %views% The number of times the video has been viewed.
146# %likes% The number of users who have "liked" the video.
147# %dislikes% The number of users who have "disliked" the video.
148# %length% Length of the video.
149# Tokens only available in the response to searches (q_resp_format):
150# %query% The original search string.
151# %res_nbr% The index of the search result being shown.
152# %res_tot% Total number of search results.
153#
154# Example:
155# "\002YouTube\002: %poster%: %youtube_url% - \"\002%title%\002\" (Uploaded by \"%author%\" on %published%) - Length: %length% - Views: %views% - Likes / Dislikes: %likes% / %dislikes%"
156# The template used when looking up a URL found in the channel
157set youtube(response_format) "\002\00301,00You\00300,04Tube\003\002: %title% - Durac: 12(%length%)"
158# The template used when replying to a search query.
159#set youtube(q_resp_format) "\002\00301,00You\00300,04Tube\003\002: %poster%: Resultado(s) %res_nbr%/%res_tot% para la busqueda '%query%': %youtube_url% - \"\002%title%\002\" (Cargado por \"%author%\" en fecha de %published%) - duracion: %length% - total veces visto: %views% - le gustan / no les gustan: %likes% / %dislikes%"
160
161# The maximum number of characters from a YouTube title to print.
162set youtube(max_title_length) 100
163
164# The maximum number of characters from a YouTube description to print.
165set youtube(max_desc_length) 200
166
167# numeros de resultados para busqueda en youtube web
168set youtube(search_res_def) 4
169
170# The maximum number of search results that can be requested with a YouTube search.
171set youtube(search_res_max) 3
172
173# The minimum interval (in seconds) a user must wait before he can execute another search query.
174# Set to 0 to disable interval checking.
175set youtube(search_int_min) 15
176
177###############################################################################
178#
179# Advanced Configuration
180# !!! DO NOT CHANGE UNLESS YOU KNOW WHAT YOU'RE DOING !!!
181#
182###############################################################################
183
184# URLs of the YouTube V3 API
185set youtube(api_get) "https://www.googleapis.com/youtube/v3/videos"
186set youtube(api_search) "https://www.googleapis.com/youtube/v3/search"
187
188# The groups of properties to be fetched
189set youtube(api_part) "snippet,statistics,contentDetails"
190
191# The fields from the selected property groups that are to be returned
192set youtube(api_fields) "items(id,snippet(publishedAt,title,description,channelTitle),statistics,contentDetails(duration))"
193
194# Maximum time in milliseconds to wait for YouTube to respond
195set youtube(api_timeout) "30000"
196
197# Pattern used to patch YouTube links in channel public text
198set youtube(pattern) {https{0,1}://.*youtu(?:\.be/|be\..*/watch\?(?:.*)v=)([A-Za-z0-9_\-]+)}
199
200###############################################################################
201
202variable packages
203
204# Check for all dependencies without killing the bot that's trying to load the script.
205if { [catch {package require Tcl 8.5} error] } { set "packages(Tcl 8.5)" $error }
206if { [catch {package require http 2.7} error] } { set "packages(http 2.7)" $error }
207if { [catch {package require tls} error] } { set packages(tls) $error }
208if { [catch {package require json} error] } { set packages(json) $error }
209
210# If any dependencies failed loading, print the error and abort.
211if { [array size packages] > 0 } {
212foreach index [array names packages] {
213putlog "% YoutubeTitleV2: Error que carga paquete '$index' : $packages($index)"
214}
215
216putlog "% YoutubeTitleV2: Carga abortó (Ausencia de dependencias)!"
217return
218}
219
220###############################################################################
221
222# We need HTTPS support for the Google APIs..
223# If local IP or host is configured in the main config, use it as the source
224# of the outgoing connections.
225if { [info exists {my-ip}] == 1 && [string length ${my-ip}] > 0} {
226http::register https 443 [list tls::socket -myaddr ${my-ip} -tls1 1]
227} elseif { [info exists {my-hostname}] == 1 && [string length ${my-hostname}] > 0} {
228http::register https 443 [list tls::socket -myaddr ${my-hostname} -tls1 1]
229} else {
230http::register https 443 [list tls::socket -tls1 1]
231}
232
233set YoutubeTitleVersion "2.1"
234
235# Keep a dictionary of the user hosts from where search queries are currently
236# ignored by the script.
237set yt_search_ignore [dict create]
238
239setudef flag youtube
240bind pubm - * public_youtube
241bind pub - !youtube youtube_query
242bind pub - !yt youtube_query
243bind pub - .youtube youtube_query
244bind pub - .yt youtube_query
245
246###############################################################################
247
248proc note {msg} {
249putlog "% $msg"
250}
251
252# Ensure strings are no longer then given length. This will cut-off the string
253# at the desired length and append '...'.
254proc shorten {text maxlen} {
255if { [string length $text] > [expr $maxlen - 1] } {
256set text [string range $text 0 [expr $maxlen - 4]]"..."
257}
258return $text
259}
260
261# Convert an ISO8601 date into a more readable format..
262proc conv_iso8601_date {orig_date} {
263global youtube
264set pattern {(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d{1}|3[012])[T\s](?:(?:([01]\d|2[0-3]):([0-5]\d))|(24):(00)):(?:([0-5]\d)(?:[\.,](\d+))?|(60)(?:[\.,](0+))?)Z}
265if { [regexp $pattern $orig_date match year month day hours minutes hours_2 minutes_2 seconds milliseconds seconds_2 milliseconds_2] } {
266# The hour and hour_2 variables are mutually exclusive, so we append the _2 variables to
267# the original ones to have fewer variables to work with.
268append hours $hours_2
269append minutes $minutes_2
270append seconds $seconds_2
271
272# Put everything in a dictionary so we can have a configurable time format.
273set tokens [dict create]
274dict set tokens %year% $year
275dict set tokens %month% $month
276dict set tokens %day% $day
277dict set tokens %hours% $hours
278dict set tokens %minutes% $minutes
279dict set tokens %seconds% $seconds
280
281return [string map $tokens $youtube(date_format)]
282} else {
283error "Incapaz de tratar valor de fecha ($orig_date) devuelto por servicio web."
284}
285}
286
287# Convert an ISO8601 duration into a more readable format..
288proc conv_iso8601_duration {duration} {
289set length ""
290set pattern {P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)}
291if { [regexp $pattern $duration match years months days hours minutes seconds] } {
292if { [string length $years] > 0 } {
293append length $years Y " "
294}
295if { [string length $months] > 0 } {
296append length $months M " "
297}
298if { [string length $days] > 0 } {
299append length $days D " "
300}
301if { [string length $hours] > 0 } {
302append length $hours h " "
303}
304if { [string length $minutes] > 0 } {
305append length $minutes m " "
306}
307if { [string length $seconds] > 0 } {
308append length $seconds s " "
309}
310} else {
311error "Incapaz de tratar valor de duración ($duration) devuelto por servicio web."
312}
313return [string trim $length]
314}
315
316###############################################################################
317
318# Process the reply string of a video lookup from the YouTube API (JSON) and add the
319# data to a dictionary containing all tokens the user will be able to use in the template.
320proc read_props {json_blob} {
321global youtube
322# Create an empty dictionary for the variables supported in the response format.
323set properties [dict create]
324
325# Convert the JSON response to a dictionary.
326set reply [json::json2dict $json_blob]
327
328# The web service returns a list of results, even though our query will always get 1.
329# So we have to take the first element from the list..
330if { ![dict exists $reply items] } {
331error "Error processing web service reply."
332} else {
333set video [lindex [dict get $reply items] 0]
334
335# Check whether the variables we support in the response are present in the
336# reply from the web service. We check this one by one instead of assuming
337# they exist in case the API changes or someone messes with the requested fields.
338# Properties of the view we extract from reply..
339if { [dict exists $video id] } {
340dict set properties %id% [dict get $video id]
341} else {
342dict set properties %id% ""
343}
344if { [dict exists $video snippet channelTitle] } {
345dict set properties %author% [dict get $video snippet channelTitle]
346} else {
347dict set properties %author% ""
348}
349if { [dict exists $video snippet title] } {
350dict set properties %title% [shorten "[dict get $video snippet title]" $youtube(max_title_length)]
351} else {
352dict set properties %title% ""
353}
354if { [dict exists $video snippet description] } {
355dict set properties %description% [shorten "[dict get $video snippet description]" $youtube(max_desc_length)]]
356} else {
357dict set properties %description% ""
358}
359if { [dict exists $video snippet publishedAt] } {
360dict set properties %published% [conv_iso8601_date [dict get $video snippet publishedAt]]
361} else {
362dict set properties %published% ""
363}
364if { [dict exists $video statistics viewCount] } {
365dict set properties %views% [dict get $video statistics viewCount]
366} else {
367dict set properties %views% ""
368}
369if { [dict exists $video statistics likeCount] } {
370dict set properties %likes% [dict get $video statistics likeCount]
371} else {
372dict set properties %likes% ""
373}
374if { [dict exists $video statistics dislikeCount] } {
375dict set properties %dislikes% [dict get $video statistics dislikeCount]
376} else {
377dict set properties %dislikes% ""
378}
379if { [dict exists $video contentDetails duration] } {
380dict set properties %length% [conv_iso8601_duration [dict get $video contentDetails duration]]
381} else {
382dict set properties %length% ""
383}
384}
385
386return $properties
387}
388
389# Process the reply string of a search query to the YouTube API (JSON) and extract the
390# video id of the results from the reply.
391proc read_searchres {json_blob} {
392global youtube
393# Create an empty list containing the video IDs returned for the search query.
394list video_ids
395
396# Convert the JSON response to a dictionary.
397set reply [json::json2dict $json_blob]
398
399# The web service returns a list of results, even though our query will always get 1.
400# So we have to take the first element from the list..
401if { ![dict exists $reply items] } {
402error "Error processing web service reply."
403} else {
404foreach res [dict get $reply items] {
405if { [dict exists $res id videoId] } {
406lappend video_ids [dict get $res id videoId]
407}
408}
409}
410
411return $video_ids
412}
413
414# Send a request to the YouTube API to fetch the video details for
415# the video with the given ID.
416proc fetch_props {youtube_id} {
417global youtube
418# Ensure an API key has been configured..
419if { [info exists youtube(api_key)] == 0 || [string length $youtube(api_key)] == 0 } {
420error "An API key must be configured to access the Google web API!"
421} else {
422set query [http::formatQuery id $youtube_id key $youtube(api_key) \
423part $youtube(api_part) fields $youtube(api_fields)]
424set response [http::geturl "$youtube(api_get)?$query" -timeout $youtube(api_timeout)]
425upvar #0 $response state
426if [expr [http::ncode $response] == 401] {
427error "La posición contuvo restringido empotran datos."
428} else {
429set response_body [http::data $response]
430http::cleanup $response
431return [read_props $response_body]
432}
433}
434}
435
436# Find the video ID of the first match for the given search.
437proc search_video {criteria results} {
438global youtube
439# Ensure an API key has been configured..
440if { [info exists youtube(api_key)] == 0 || [string length $youtube(api_key)] == 0 } {
441error "An API key must be configured to access the Google web API!"
442} else {
443set query [http::formatQuery type "video" q $criteria key $youtube(api_key) \
444part "id" fields "items(id(videoId))" maxResults "$results"]
445set response [http::geturl "$youtube(api_search)?$query" -timeout $youtube(api_timeout)]
446upvar #0 $response state
447if [expr [http::ncode $response] == 401] {
448error "Location contained restricted embed data."
449} else {
450set response_body [http::data $response]
451http::cleanup $response
452return [read_searchres $response_body]
453}
454}
455}
456
457###############################################################################
458
459# This is triggered to analyse ever channel message for the presence of the YouTube URL.
460# When one is found, the ID is extracted and passed on to get a list of the video properties.
461# Finally, this list is used to fill in the tokens in the user defined reply template.
462proc public_youtube {nick userhost handle channel args} {
463global youtube botnick
464
465if { [channel get $channel youtube] && [regexp -nocase -- $youtube(pattern) $args match video_id] } {
466if { [catch {set tokens [fetch_props $video_id]} error] } {
467note "Failed to get video details: $error (querying '$video_id')"
468# If the reply contained an empty ID, we assume we found no video..
469} elseif { [string length [dict get $tokens %id%]] == 0 } {
470putserv "PRIVMSG $channel :Incapaz de encontrar un vídeo youtube con ID '$video_id'."
471} else {
472dict set tokens %botnick% $botnick
473dict set tokens %poster% $nick
474# Rebuild the URL so we use a url shortener or force SSL
475# in all messages coming from us
476dict set tokens %youtube_url% "$youtube(base_url)$video_id"
477
478set result [string map $tokens $youtube(response_format)]
479putserv "PRIVMSG $channel :[encoding convertfrom utf-8 $result]"
480}
481}
482}
483
484# This is triggered on !youtube commands.
485# Allows turning monitoring on or off by admins.
486# All other queries are interpreted as a youtube search.
487proc youtube_query {nick userhost handle channel largs} {
488global youtube botnick yt_search_ignore
489
490# We get a list of arguments, join it to get rid of the curly braces..
491set args [join $largs]
492
493if { [string length $args] == 0 } {
494if { [channel get $channel youtube] } {
495putserv "PRIVMSG $channel :Syntax: \002!youtube <search criteria>\002 - Search for a video showing the top $youtube(search_res_def) result(s)."
496putserv "PRIVMSG $channel :Syntax: \002!youtube +<number of results> <search criteria>\002 - Search for a video showing the top results. (Shows max $youtube(search_res_max) result(s).)"
497}
498if { [matchattr $handle +mno|+mno $channel] } {
499putserv "NOTICE $nick :Syntax: \002!youtube <on/off>\002 - Turn youtube link lookups on/off."
500}
501} elseif { [matchattr $handle +mno|+mno $channel] && ([string compare $args "on"] == 0 \
502|| [string compare $args "off"] == 0) } {
503if { ![channel get $channel youtube] && [string compare $args "on"] == 0 } {
504channel set $channel +youtube
505putserv "NOTICE $nick :YoutubeTitleV2: activado en $channel"
506note "YoutubeTitleV2: Monitoring enabled by $nick for $channel."
507} elseif { [channel get $channel youtube] && [string compare $args "off"] == 0 } {
508channel set $channel -youtube
509putserv "NOTICE $nick :YoutubeTitleV2: disabled on $channel"
510note "YoutubeTitleV2: Monitoring disabled by $nick for $channel."
511}
512} elseif { [channel get $channel youtube] } {
513# Stores the number of search results to be fetched.
514set nbr $youtube(search_res_def)
515
516# Check if the user requested a specific number of search results.
517if { [string first + $args] == 0 && [string is digit [string range [lindex $largs 0] 1 end]] == 1 } {
518set nbr [string range [lindex $largs 0] 1 end]
519set args [join [lrange $largs 1 end]]
520}
521
522# Ensure no more then the maximum number of search results is fetched
523if {$nbr > $youtube(search_res_max) } {
524set nbr $youtube(search_res_max)
525}
526
527# The magic number comes from the length of the string "cat".. ;)
528if { [string length $args] < 3 } {
529putserv "PRIVMSG $channel :Search criteria must be at least 3 characters long."
530return 0
531}
532
533# First verify whether the user isn't on ignore..
534if { $youtube(search_int_min) > 0 } {
535# Don't just check for the existence of the key, but also verify the expiry time
536# set, in case for some reason the key didn't get removed when it should have been..
537if { [dict exists $yt_search_ignore $userhost] && [dict get $yt_search_ignore $userhost] > [unixtime] } {
538putserv "NOTICE $nick :YoutubeTitleV2: Please wait $youtube(search_int_min) seconds between searches."
539return 0
540}
541
542# Add the user host to the YouTube search ignore list to ignore any queries for X seconds.
543dict set yt_search_ignore $userhost [expr [unixtime] + $youtube(search_int_min)]
544# Set a timer to remove the user host from the list after X seconds.
545utimer $youtube(search_int_min) [list dict unset yt_search_ignore $userhost]
546}
547
548# Search a video...
549# Note that we have to do 2 requests: one to fetch search results (id)
550# and a second to get video details. This is caused by the YouTube API
551# not being capable of returning details in the search functions.
552if { [catch { set video_ids [search_video $args $nbr] } error ] } {
553note "Failed to find a video: $error (searching for '$args')."
554# If the reply contained an empty ID, we assume we found no video..
555} elseif { [llength $video_ids] == 0 } {
556putserv "PRIVMSG $channel :Unable to find a YouTube video matching search '$args'"
557} else {
558# Go over each video ID returned by our search query..
559set index 1
560foreach video_id $video_ids {
561# We have the video id, now find the properties..
562if { [catch {set tokens [fetch_props $video_id]} error] } {
563note "Failed to get video details: $error (searching for '$args' and found '$video_id')."
564# If the reply contained an empty ID, we assume we found no video..
565} elseif { [string length [dict get $tokens %id%]] == 0 } {
566putserv "PRIVMSG $channel :Unable to fetch the video details of '$video_id' for the search result '$args'"
567} else {
568dict set tokens %botnick% $botnick
569dict set tokens %poster% $nick
570dict set tokens %query% $args
571dict set tokens %res_nbr% $index
572dict set tokens %res_tot% [llength $video_ids]
573# Rebuild the URL so we use a url shortener or force SSL
574# in all messages coming from us
575dict set tokens %youtube_url% "$youtube(base_url)$video_id"
576
577set result [string map $tokens $youtube(q_resp_format)]
578putserv "PRIVMSG $channel :$result"
579}
580incr index
581}
582}
583}
584}
585
586###############################################################################
587
588note "YoutubeTitleV2 Version $YoutubeTitleVersion: loaded";