diff --git a/README.md b/README.md index ba2a51d..4f1ac88 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,324 @@ -# tabchi -An Advertiser Telgram bot in Lua +

+

تبلیغ‌گر +

سرعت 💠 دقت 💠 قدرت +

+ + + + + + + + + +
+ +

تبلیغ‌گر رباتی هوشمند, بسیار ساده و البته رایگان جهت امور تبلیغاتی در تلگرام است.
+نوشته شده توسط ناجی برپایه اخرین نسخه تلگرام. +
+

نصب و راه‌اندازی 🚀 +

+
+

ابتدا سورس ربات تبلیغ‌گر را کپی کرده و پیش‌زمینه‌ها را نصب کنید.

+
(موارد زیر را در ترمینال وارد کنید)
+
+git clone https://github.com/i-naji/tabchi.git -b persian
+cd tabchi
+chmod +x bot
+./bot install
+
+

برای ساخت ربات جدید bot create/. را وارد کنید. +

+
+./bot create
+
+

حال با توجه به شناسه ای که برای ربات جدید داده شده تبلیغ‌گر خود را راه‌اندازی کنید. +
مثال:

+
در صورتی ک ردیس تنظیم نشده است آن را تنظیم کنید.
+
+دستوری برای راه اندازی مجدد ردیس#
+sudo service redis-server restart
+دستوری برای راه اندازی ردیس#
+sudo service redis-server start
+
+./bot 1
+
+
توجه داشته باشید برای اولین بار که ربات را راه اندازی می‌کنید،از شما شناسه عددی مدیر ربات (شما و یا هرکس که می خواهید مدیر ربات شود) ، شماره ربات و کد ورود به حساب کاربری خواسته می‌شود. +
شما می توانید با استفاده از ربات @ID_ProBot شناسه عددی خود را بدست آورید.
+
از راهنمای‌تبلیغ‌گر برای آشنا شدن با طرز کار رباتتان استفاده کنید.
+
+

جلوگیری از قطع شدن عملکرد تبلیغ‌گر🛡 +
+

یکی از مشکلات کار با SSH، قطع شدن آن در زمان قطع اتصال اینترنت است.
وقتی اتصال اینترنت قطع می‌شود اجرای تمامی برنامه‌ها و فرامینی که در حال استفاده از SSH بودند، متوقف می‌شود. فرمان screen این‌جا به‌کمک شما می‌آید. کافی است این دستور را قبل از دستورراه‌اندازی تبلیغ‌گر قرار دهید.

+
مثال:
+
+screen ./bot 1
+
+

برای خارج شدن از محیط screen کلید‌های ترکیبی Ctrl+A و سپس کلید D را بفشارید.
برای مشاهده فهرست screen های موجود می‌توانید از دستور screen -ls استفاده کنید.
این دستور فهرست تمامی screen های در حال اجرا را نمایش می‌دهد.
برای مشاهده screen اجرا‌شده، کافی است دستور زیر را وارد کنید:

+
+screen -r [screen name]
+
+
+

✯ راهنمای تبلیغ‌گر :

+ + + + + + + + + + + + +     + + + +     + + + +     + + + +     + + + +     + + + + + + + + + + + + + + + + + + +     + + + + +     + + + + +     + + + +     +     + + + + +     +     + + + +     +     + + + +     +     + + + +     + + + + +     + + + + + + + +     + + + + + + + +     + + + + +     + + + + + + +     +     + + + + +     + + + + +     + + + + + + + + + + + + + + + + + + + +     + + + + +     + + + + +     + + + + + + +     + + + +     + + + + + + + + + + + + + + + + + + + +     + + + + +     + + + + +     + + + +     + + + + + + +     + +
مخصوص مدیر
دستورعملکردفقط در گروه (✤)
فقط در ریپلای (↻)
reload/بارگذاری مجدد ربات و راه‌اندازی دوباره
بروزرسانی رباتبروزرسانی ربات به آخرین نسخه
و بارگذاری مجدد
تازه سازیتازه سازی آمار تبیلغ‌گر
(حدکثر یک بار در روز)
ریپورتاطلاع از وضعیت ریپورت ربات
تازه سازی رباتتازه سازی اطلاعات فردی تبلیغ‎گر
بعد از تغییر در مشخصات فردی تبلیغ‎گر
مانند اسم انجام گیرد
افزودن مدیرشناسهافزودن مدیر با شناسه وارد شد
افزودن مدیرکلشناسهافزودن مدیرکل با شناسه وارد شد
حذف مدیرشناسهحذف مدیر یا مدیر کل با شناسه خاص
ترک کردنخارج شدن از گروه و حذف
از اطلاعات گروه‌ها
افزودن همه مخاطبینافزودن تمام مخاطبین و افراد در گفت‌و‎گوی شخصی
به گروه
شناسه منارسال شناسه عددی شما
بگومتنارسال متن
ارسال کن"شناسه" متنارسال متن به شناسه گروه یا فرد
تنظیم نام"نام" نام‌خانوادگیتغییر نام
تنطیم نام کاربرینام کاربریجایگزینی نام کاربری داده شده
(دفعات استفاده محدود در زمان کوتاه)
حذف نام کاربریحذف نام کاربری
(دفعات استفاده محدود در زمان کوتاه)
افزودن با شمارهروشنتغییر وضعیت اشتراک شماره تبلیغ‌گر
در جواب شماره به اشتراک گذاشته شده
خاموش
افزودن با پیامروشنتغییر وضعیت ارسال پیام در جواب
شماره به اشتراک گذاشته شده
خاموش
تنظیم پیام افزودن مخاطبمتنتنظیم متن داده شده به عنوان جواب
شماره به اشتراک گذاشته شده
پاسخگوی خودکارروشنتغییر وضعیت پاسخ ‌گویی خودکار
خاموش
تنظیم جواب"متن" جوابتنظیم جوابی به عنوان پاسخ خودکار
به پیام وارد شده مطابق با متن
حذف جوابمتنحذف جواب مربوط به متن
لیستمخاطبینارسال لیستی از مورد خواسته شده
در قالب پرونده متنی یا پیام
خصوصی
گروه
سوپرگروه
لینک
پاسخ های خودکار
مسدودیتشناسهمسدود‌کردن(بلاک) کاربر با شناسه داده شده
رفع مسدودیتشناسهرفع مسدودیت کاربر با شناسه داده شده
وضعیت مشاهدهروشنخاموش و یا روشن‌کردن وضعیت مشاهده پیام‌ها
(فعال و غیر‌فعال‌کردن تیک دوم)
خاموش
امارارسال آمار و وضعیت تبلیغ‌گر
وضعیتارسال وضعیت اجرایی تبلیغ‌گر
ارسال بههمهارسال پیام جواب داده شده
به مورد خواسته شده
خصوصی
گروه
سوپرگروه
ارسال به سوپرگروهمتنارسال متن داده شده به تمام سوپرگروه ها
ترک کردنشناسهخروج از گروه با‌شناسه وارد شده
و حذف از اطلاعات گروه
افزودن به همهشناسهدعوت کردن کاربر با شناسه وارد شده به تمامی
گروه ها و سوپرگروه ها
راهنمادریافت راهنمای تبلیغ‌گر
استفاده از اطلاعات تبچی
همگام سازی با تبچیبه آموزش موجود در کانال سر بزنید..
+
اساس کار تبلیغ‌گر تشخیص لینک و عضویت در هر گروه‌ها و پیروی از دستورات تعیین شده است.
+
+

نسخه ➀ :

+
+

امکاناتی که در نسخه ➀ افزوده خواهند شد + انتشار نسخه رسمی : +
▣ پایه ای با برنامه نویسی خلاصه تر و سریعتر +
▣ حذف و اضافه کردن عکس پروفایل +
▣ عملیات ارسال رگباری +
▣ چک کردن گروه‌ها و ثبت مجدد لیست امار +

+
+

در آینده :

+
+

امکاناتی که پروژه تبلیغ‌گر بعد ها به خود خواهد دید +
▣ تعیین حد مجاز برای عضویت در گروه ها +
▣ خارج شدن از گروه در صورت اسپم +
▣ مسدود کردن کاربر در صورت اسپم +
▢ تغییر شماره توسط مدیر +
▢ حذف حساب کاربری تبلیغ‎گر توسط مدیر +
▢ افزودن و یا تغییر ایمیل تایید دو مرحله ای +
▢ غیر فعال کردن دیگر نسشت ها +
▢ عضویت در گروه ها با حداقل و یا حداکثر ظرفیت مشخص +
▢ خارج شدن از گروه در صورت عدم فعالیت در بازه زمانی مشخص +
▢ فرستادن نوع خاصی از رسانه دریافت شده به گروه مدیریت +
▢ ربات کمک رسان با قابلیت سازماندهی(ارسال پیام در زمان خاص,کیبورد شیشه ای) +
▢ سازماندهی چند تبلیغ‌گر جهت جلوگیری از عضویت در گروه های یکسان +
▢ فرستادن پیام ها از یک گفت و گوی خصوصی خاص +
. +
. +
. +

+
+

🆓بهترین سرور‌های رایگان +
+ + + + + + + + + +
+
+

پشتیبانی +
+

پذیرای انتقادات وپیشنهاداتان هستیم ♚ NajiTeam ♚ @i_naji @Naji_MessengerRobot +
راهنمای استفاده از تبلیغ‌گر بسیار ساده و قابل درک است
فیلم آموزشی نصب و اطلاع از آخرین اخبار تبلیغ‌گر در کانال ما + @i_advertiser +

diff --git a/bot b/bot new file mode 100644 index 0000000..f113876 --- /dev/null +++ b/bot @@ -0,0 +1,195 @@ +#!/usr/bin/env bash + +## VARIABLES +THIS_DIR=$(cd "$(dirname "$0")"; pwd) +TDCLI='https://valtman.name/files/telegram-cli-1222' + +## Print Colorful +# Print text in red +prtred() { + printf '\e[1;31m%s\n\e[0;39;49m' "$@" +} +# Print text in green +prtgrn() { + printf '\e[1;32m%s\n\e[0;39;49m' "$@" +} +# Print text in brown +prtbrown() { + printf '\e[1;33m%s\n\e[0;39;49m' "$@" +} +# update data to the last version +update() { + git pull -f + prtgrn " + بروزرسانی اطلاعات با موفقیت انجام شد << + >> Bot's source successfully updated +" +} +# Create a new Bot +create() { + name=bot + if [[ -e $name.lua ]] ; then + i=1 + while [[ -e $name-$i.lua ]] ; do + let i++ + done + name=$name-$i + fi + cat bot.lua >> "$name".lua + sed -i 's/BOT-ID/'$i'/g' "$name".lua + prtgrn " + ربات شماره "$1" ساخته شد << + : رباتتان را با فرمان زیر اجرا کنید +>> new bot seccessfuly created + bot number "$i" + run your bot by :" +prtred " + ./bot "$i" +" +} +# Reset data to the last update +fix() { + git reset --hard FETCH_HEAD + prtgrn ' + بازیابی اطلاعات به آخرین آپدیت انجام شد << + >> Database Reseted and Fixed +' +} +# autolauncher +autolaunch() { + while true ; do + for tablighgar in bot-*.lua ; do + tab="${tablighgar%.*}" + ltab="${tab/-/ }" + tmux kill-session -t $tab + for tg in ~/.telegram-cli/$tab/data/* ; do + rm -rf $tg/* + done + TMUX= tmux new-session -d -s $tab "./$ltab" + tmux detach -s $tab + done + printf '\n\e[1;32mربات ها راه اندازی شدند << \e[1;34m|Naji| \e[1;32m>> Bots are Running\n\e[0;39;49m' + sleep 1800 + done +} +# install Bot +install() { + prtgrn ' + آیا قصد نصب پیش نیاز های ربات تبلیغگر را دارید؟ بله|خیر << + >> Do you want to install Essentials of Tabchi bot? (Y/N): +' + read -rp ' ' install + case "$install" in + Y|y|بله) + prtgrn " + telegram-cli بارگیری << + >> Fetching $TDCLI +" + wget "$TDCLI" -O telegram-cli + chmod +x telegram-cli + prtgrn " + ارتقای اطلاعات قدیمی << + >> Updating old packages +" + sudo apt-get -y update && sudo apt-get -y upgrade + prtgrn " + نصب بسته های پیش نیازی << + >> Installing Essentials packages +" + sudo apt-get --force-yes install git wget screen tmux libconfig9 libevent-dev libjansson4 libstdc++6 lua-socket lua5.2 liblua5.2 make unzip redis-server software-properties-common g++ + sudo apt-get -y update && sudo apt-get -y upgrade + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test && sudo apt-get update && sudo apt-get install -y gcc-4.9 g++-4.9 && sudo update-alternatives —install /usr/bin/gcc gcc /usr/bin/gcc-4.9 60 —slave /usr/bin/g++ g++ /usr/bin/g++-4.9 + prtgrn " + ارتقای بسته های نصب شده << + >> Updating packages +" + sudo apt-get -y update && sudo apt-get -y upgrade && sudo apt-get -y dist-upgrade && sudo apt-get -y autoremove + prtgrn " + redis راه اندازی مجدد << + >> Restarting redis service +" + sudo service redis-server restart + prtgrn ' + همگام سازی با جدیدترین نسخه تبلیغگر << + >> Fetching latest Bot source code + ' + git pull + prtgrn ' + نصب پیش نیاز های ربات تبلیغگر با موفقیت انجام شد << + >> Essentials of Tablighgar Bot successfully installed! + ' + printf '\e[1;33mبسازید\e[1;32m ./bot create \e[1;33mربات تبلیغگر خود را با دستور <<\e[1;33m%s\n >>Create Your bot with\e[1;32m ./bot create\e[0;39;49m%s\n' + ;; + N|n|خیر) + prtbrown ' + لغو عملیات + Canceling the operation + ' + ;; + *) + prtred ' + دستور اشتباه + Wrong command + ' + install + ;; + esac +} +# How to use this script +usage() { +printf "\e[1;36m" + cat <> Usage: $0 [options] + Options: + unmber Start bot whit this ID number + install Install of Tabchi Bot + create Create a new Tabchi Bot + update Update bot source code + help Print this message + fix Reseting data +EOF +printf "%s\n\e[0;39;49m" +} +## MAIN ------------------------------------------------------------------------ +# Make sure this script run inside Bot directory +cd "$THIS_DIR" || exit + +case $1 in + update) + update + ;; + create) + create + ;; + install) + install + ;; + fix) + fix + ;; + autolaunch) + autolaunch + ;; + help) + usage + ;; + *) + if [ -a "$THIS_DIR"/bot-"$1".lua ]; then + while true; do + ./telegram-cli -p bot-"$1" -s bot-"$1".lua + done + else + usage + fi + ;; +esac diff --git a/bot.lua b/bot.lua new file mode 100644 index 0000000..c30c379 --- /dev/null +++ b/bot.lua @@ -0,0 +1,701 @@ +redis = (loadfile "redis.lua")() +redis = redis.connect('127.0.0.1', 6379) + +function dl_cb(arg, data) +end +function get_admin () + if redis:get('botBOT-IDadminset') then + return true + else + print("\n\27[32m لازمه کارکرد صحیح ، فرامین و امورات مدیریتی ربات تبلیغ گر <<\n تعریف کاربری به عنوان مدیر است\n\27[34m ایدی خود را به عنوان مدیر وارد کنید\n\27[32m شما می توانید از ربات زیر شناسه عددی خود را بدست اورید\n\27[34m ربات: @id_ProBot") + print("\n\27[32m >> Tabchi Bot need a fullaccess user (ADMIN)\n\27[34m Imput Your ID as the ADMIN\n\27[32m You can get your ID of this bot\n\27[34m @id_ProBot") + print("\n\27[36m : شناسه عددی ادمین را وارد کنید << \n >> Imput the Admin ID :\n\27[31m ") + admin=io.read() + redis:del("botBOT-IDadmin") + redis:sadd("botBOT-IDadmin", admin) + redis:set('botBOT-IDadminset',true) + end + return print("\n\27[36m ADMIN ID |\27[32m ".. admin .." \27[36m| شناسه ادمین") +end +function get_bot (i, naji) + function bot_info (i, naji) + redis:set("botBOT-IDid",naji.id_) + if naji.first_name_ then + redis:set("botBOT-IDfname",naji.first_name_) + end + if naji.last_name_ then + redis:set("botBOT-IDlanme",naji.last_name_) + end + redis:set("botBOT-IDnum",naji.phone_number_) + return naji.id_ + end + tdcli_function ({ID = "GetMe",}, bot_info, nil) +end +function reload(chat_id,msg_id) + loadfile("./bot-BOT-ID.lua")() + send(chat_id, msg_id, "با موفقیت انجام شد.") +end +function is_naji(msg) + local var = false + local hash = 'botBOT-IDadmin' + local user = msg.sender_user_id_ + local Naji = redis:sismember(hash, user) + if Naji then + var = true + end + return var +end +function writefile(filename, input) + local file = io.open(filename, "w") + file:write(input) + file:flush() + file:close() + return true +end +function process_join(i, naji) + if naji.code_ == 429 then + local message = tostring(naji.message_) + local Time = message:match('%d+') + redis:setex("botBOT-IDmaxjoin", tonumber(Time), true) + else + redis:srem("botBOT-IDgoodlinks", i.link) + redis:sadd("botBOT-IDsavedlinks", i.link) + end +end +function process_link(i, naji) + if (naji.is_group_ or naji.is_supergroup_channel_) then + redis:srem("botBOT-IDwaitelinks", i.link) + redis:sadd("botBOT-IDgoodlinks", i.link) + elseif naji.code_ == 429 then + local message = tostring(naji.message_) + local Time = message:match('%d+') + redis:setex("botBOT-IDmaxlink", tonumber(Time), true) + else + redis:srem("botBOT-IDwaitelinks", i.link) + end +end +function find_link(text) + if text:match("https://telegram.me/joinchat/%S+") or text:match("https://t.me/joinchat/%S+") or text:match("https://telegram.dog/joinchat/%S+") then + local text = text:gsub("t.me", "telegram.me") + local text = text:gsub("telegram.dog", "telegram.me") + for link in text:gmatch("(https://telegram.me/joinchat/%S+)") do + if not redis:sismember("botBOT-IDalllinks", link) then + redis:sadd("botBOT-IDwaitelinks", link) + redis:sadd("botBOT-IDalllinks", link) + end + end + end +end +function add(id) + local Id = tostring(id) + if not redis:sismember("botBOT-IDall", id) then + if Id:match("^(%d+)$") then + redis:sadd("botBOT-IDusers", id) + redis:sadd("botBOT-IDall", id) + elseif Id:match("^-100") then + redis:sadd("botBOT-IDsupergroups", id) + redis:sadd("botBOT-IDall", id) + else + redis:sadd("botBOT-IDgroups", id) + redis:sadd("botBOT-IDall", id) + end + end + return true +end +function rem(id) + local Id = tostring(id) + if redis:sismember("botBOT-IDall", id) then + if Id:match("^(%d+)$") then + redis:srem("botBOT-IDusers", id) + redis:srem("botBOT-IDall", id) + elseif Id:match("^-100") then + redis:srem("botBOT-IDsupergroups", id) + redis:srem("botBOT-IDall", id) + else + redis:srem("botBOT-IDgroups", id) + redis:srem("botBOT-IDall", id) + end + end + return true +end +function send(chat_id, msg_id, text) + tdcli_function ({ + ID = "SendMessage", + chat_id_ = chat_id, + reply_to_message_id_ = msg_id, + disable_notification_ = 1, + from_background_ = 1, + reply_markup_ = nil, + input_message_content_ = { + ID = "InputMessageText", + text_ = text, + disable_web_page_preview_ = 1, + clear_draft_ = 0, + entities_ = {}, + parse_mode_ = {ID = "TextParseModeHTML"}, + }, + }, dl_cb, nil) +end +get_admin() +function tdcli_update_callback(data) + if data.ID == "UpdateNewMessage" then + if not redis:get("botBOT-IDmaxlink") then + if redis:scard("botBOT-IDwaitelinks") ~= 0 then + local links = redis:smembers("botBOT-IDwaitelinks") + for x,y in pairs(links) do + if x == 11 then redis:setex("botBOT-IDmaxlink", 60, true) return end + tdcli_function({ID = "CheckChatInviteLink",invite_link_ = y},process_link, {link=y}) + end + end + end + if not redis:get("botBOT-IDmaxjoin") then + if redis:scard("botBOT-IDgoodlinks") ~= 0 then + local links = redis:smembers("botBOT-IDgoodlinks") + for x,y in pairs(links) do + tdcli_function({ID = "ImportChatInviteLink",invite_link_ = y},process_join, {link=y}) + if x == 5 then redis:setex("botBOT-IDmaxjoin", 60, true) return end + end + end + end + local msg = data.message_ + local bot_id = redis:get("botBOT-IDid") or get_bot() + if (msg.sender_user_id_ == 777000 or msg.sender_user_id_ == 178220800) then + for k,v in pairs(redis:smembers('botBOT-IDadmin')) do + tdcli_function({ + ID = "ForwardMessages", + chat_id_ = v, + from_chat_id_ = msg.chat_id_, + message_ids_ = {[0] = msg.id_}, + disable_notification_ = 0, + from_background_ = 1 + }, dl_cb, nil) + end + end + if tostring(msg.chat_id_):match("^(%d+)") then + if not redis:sismember("botBOT-IDall", msg.chat_id_) then + redis:sadd("botBOT-IDusers", msg.chat_id_) + redis:sadd("botBOT-IDall", msg.chat_id_) + end + end + add(msg.chat_id_) + if msg.date_ < os.time() - 5 then + return false + end + if msg.content_.ID == "MessageText" then + local text = msg.content_.text_ + local matches + find_link(text) + if is_naji(msg) then + if text:match("^(افزودن مدیر) (%d+)$") then + local matches = text:match("%d+") + if redis:sismember('botBOT-IDadmin', matches) then + return send(msg.chat_id_, msg.id_, "کاربر مورد نظر در حال حاضر مدیر است.") + elseif redis:sismember('botBOT-IDmod', msg.sender_user_id_) then + return send(msg.chat_id_, msg.id_, "شما دسترسی ندارید.") + else + redis:sadd('botBOT-IDadmin', matches) + redis:sadd('botBOT-IDmod', matches) + return send(msg.chat_id_, msg.id_, "مقام کاربر به مدیر ارتقا یافت") + end + elseif text:match("^(افزودن مدیرکل) (%d+)$") then + local matches = text:match("%d+") + if redis:sismember('botBOT-IDmod',msg.sender_user_id_) then + return send(msg.chat_id_, msg.id_, "شما دسترسی ندارید.") + end + if redis:sismember('botBOT-IDmod', matches) then + redis:srem("botBOT-IDmod",matches) + redis:sadd('botBOT-IDadmin'..tostring(matches),msg.sender_user_id_) + return send(msg.chat_id_, msg.id_, "مقام کاربر به مدیریت کل ارتقا یافت .") + elseif redis:sismember('botBOT-IDadmin',matches) then + return send(msg.chat_id_, msg.id_, 'درحال حاضر مدیر هستند.') + else + redis:sadd('botBOT-IDadmin', matches) + redis:sadd('botBOT-IDadmin'..tostring(matches),msg.sender_user_id_) + return send(msg.chat_id_, msg.id_, "کاربر به مقام مدیرکل منصوب شد.") + end + elseif text:match("^(حذف مدیر) (%d+)$") then + local matches = text:match("%d+") + if redis:sismember('botBOT-IDmod', msg.sender_user_id_) then + if tonumber(matches) == msg.sender_user_id_ then + redis:srem('botBOT-IDadmin', msg.sender_user_id_) + redis:srem('botBOT-IDmod', msg.sender_user_id_) + return send(msg.chat_id_, msg.id_, "شما دیگر مدیر نیستید.") + end + return send(msg.chat_id_, msg.id_, "شما دسترسی ندارید.") + end + if redis:sismember('botBOT-IDadmin', matches) then + if redis:sismember('botBOT-IDadmin'..msg.sender_user_id_ ,matches) then + return send(msg.chat_id_, msg.id_, "شما نمی توانید مدیری که به شما مقام داده را عزل کنید.") + end + redis:srem('botBOT-IDadmin', matches) + redis:srem('botBOT-IDmod', matches) + return send(msg.chat_id_, msg.id_, "کاربر از مقام مدیریت خلع شد.") + end + return send(msg.chat_id_, msg.id_, "کاربر مورد نظر مدیر نمی باشد.") + elseif text:match("^(تازه سازی ربات)$") then + get_bot() + return send(msg.chat_id_, msg.id_, "مشخصات فردی ربات بروز شد.") + elseif text:match("ریپورت") then + tdcli_function ({ + ID = "SendBotStartMessage", + bot_user_id_ = 178220800, + chat_id_ = 178220800, + parameter_ = 'start' + }, dl_cb, nil) + elseif text:match("^(/reload)$") then + return reload(msg.chat_id_,msg.id_) + elseif text:match("^بروزرسانی ربات$") then + io.popen("git fetch --all && git reset --hard origin/persian && git pull origin persian && chmod +x bot"):read("*all") + local text,ok = io.open("bot.lua",'r'):read('*a'):gsub("BOT%-ID",BOT-ID) + io.open("bot-BOT-ID.lua",'w'):write(text):close() + return reload(msg.chat_id_,msg.id_) + elseif text:match("^همگام سازی با تبچی$") then + local botid = BOT-ID - 1 + redis:sunionstore("botBOT-IDall","tabchi:"..tostring(botid)..":all") + redis:sunionstore("botBOT-IDusers","tabchi:"..tostring(botid)..":pvis") + redis:sunionstore("botBOT-IDgroups","tabchi:"..tostring(botid)..":groups") + redis:sunionstore("botBOT-IDsupergroups","tabchi:"..tostring(botid)..":channels") + redis:sunionstore("botBOT-IDsavedlinks","tabchi:"..tostring(botid)..":savedlinks") + return send(msg.chat_id_, msg.id_, "همگام سازی اطلاعات با تبچی شماره "..tostring(botid).." انجام شد.") + elseif text:match("^(لیست) (.*)$") then + local matches = text:match("^لیست (.*)$") + local naji + if matches == "مخاطبین" then + return tdcli_function({ + ID = "SearchContacts", + query_ = nil, + limit_ = 999999999 + }, + function (I, Naji) + local count = Naji.total_count_ + local text = "مخاطبین : \n" + for i =0 , tonumber(count) - 1 do + local user = Naji.users_[i] + local firstname = user.first_name_ or "" + local lastname = user.last_name_ or "" + local fullname = firstname .. " " .. lastname + text = tostring(text) .. tostring(i) .. ". " .. tostring(fullname) .. " [" .. tostring(user.id_) .. "] = " .. tostring(user.phone_number_) .. " \n" + end + writefile("botBOT-ID_contacts.txt", text) + tdcli_function ({ + ID = "SendMessage", + chat_id_ = I.chat_id, + reply_to_message_id_ = 0, + disable_notification_ = 0, + from_background_ = 1, + reply_markup_ = nil, + input_message_content_ = {ID = "InputMessageDocument", + document_ = {ID = "InputFileLocal", + path_ = "botBOT-ID_contacts.txt"}, + caption_ = "مخاطبین تبلیغ‌گر شماره BOT-ID"} + }, dl_cb, nil) + return io.popen("rm -rf botBOT-ID_contacts.txt"):read("*all") + end, {chat_id = msg.chat_id_}) + elseif matches == "پاسخ های خودکار" then + local text = "لیست پاسخ های خودکار :\n\n" + local answers = redis:smembers("botBOT-IDanswerslist") + for k,v in pairs(answers) do + text = tostring(text) .. "l" .. tostring(k) .. "l " .. tostring(v) .. " : " .. tostring(redis:hget("botBOT-IDanswers", v)) .. "\n" + end + if redis:scard('botBOT-IDanswerslist') == 0 then text = " EMPTY" end + return send(msg.chat_id_, msg.id_, text) + elseif matches == "مسدود" then + naji = "botBOT-IDblockedusers" + elseif matches == "شخصی" then + naji = "botBOT-IDusers" + elseif matches == "گروه" then + naji = "botBOT-IDgroups" + elseif matches == "سوپرگروه" then + naji = "botBOT-IDsupergroups" + elseif matches == "لینک" then + naji = "botBOT-IDsavedlinks" + elseif matches == "مدیر" then + naji = "botBOT-IDadmin" + else + return true + end + local list = redis:smembers(naji) + local text = tostring(matches).." : \n" + for i, v in pairs(list) do + text = tostring(text) .. tostring(i) .. "- " .. tostring(v).."\n" + end + writefile(tostring(naji)..".txt", text) + tdcli_function ({ + ID = "SendMessage", + chat_id_ = msg.chat_id_, + reply_to_message_id_ = 0, + disable_notification_ = 0, + from_background_ = 1, + reply_markup_ = nil, + input_message_content_ = {ID = "InputMessageDocument", + document_ = {ID = "InputFileLocal", + path_ = tostring(naji)..".txt"}, + caption_ = "لیست "..tostring(matches).." های تبلیغ گر شماره BOT-ID"} + }, dl_cb, nil) + return io.popen("rm -rf "..tostring(naji)..".txt"):read("*all") + elseif text:match("^(وضعیت مشاهده) (.*)$") then + local matches = text:match("^وضعیت مشاهده (.*)$") + if matches == "روشن" then + redis:set("botBOT-IDmarkread", true) + return send(msg.chat_id_, msg.id_, "وضعیت پیام ها >> خوانده شده ✔️✔️\n(تیک دوم فعال)") + elseif matches == "خاموش" then + redis:del("botBOT-IDmarkread") + return send(msg.chat_id_, msg.id_, "وضعیت پیام ها >> خوانده نشده ✔️\n(بدون تیک دوم)") + end + elseif text:match("^(افزودن با پیام) (.*)$") then + local matches = text:match("^افزودن با پیام (.*)$") + if matches == "روشن" then + redis:set("botBOT-IDaddmsg", true) + return send(msg.chat_id_, msg.id_, "پیام افزودن مخاطب فعال شد") + elseif matches == "خاموش" then + redis:del("botBOT-IDaddmsg") + return send(msg.chat_id_, msg.id_, "پیام افزودن مخاطب غیرفعال شد") + end + elseif text:match("^(افزودن با شماره) (.*)$") then + local matches = text:match("افزودن با شماره (.*)$") + if matches == "روشن" then + redis:set("botBOT-IDaddcontact", true) + return send(msg.chat_id_, msg.id_, "ارسال شماره هنگام افزودن مخاطب فعال شد") + elseif matches == "خاموش" then + redis:del("botBOT-IDaddcontact") + return send(msg.chat_id_, msg.id_, "ارسال شماره هنگام افزودن مخاطب غیرفعال شد") + end + elseif text:match("^(تنظیم پیام افزودن مخاطب) (.*)") then + local matches = text:match("^تنظیم پیام افزودن مخاطب (.*)") + redis:set("botBOT-IDaddmsgtext", matches) + return send(msg.chat_id_, msg.id_, "پیام افزودن مخاطب ثبت شد :\n🔹 "..matches.." 🔹") + elseif text:match('^(تنظیم جواب) "(.*)" (.*)') then + local txt, answer = text:match('^تنظیم جواب "(.*)" (.*)') + redis:hset("botBOT-IDanswers", txt, answer) + redis:sadd("botBOT-IDanswerslist", txt) + return send(msg.chat_id_, msg.id_, "جواب برای | " .. tostring(txt) .. " | تنظیم شد به :\n" .. tostring(answer)) + elseif text:match("^(حذف جواب) (.*)") then + local matches = text:match("^حذف جواب (.*)") + redis:hdel("botBOT-IDanswers", matches) + redis:srem("botBOT-IDanswerslist", matches) + return send(msg.chat_id_, msg.id_, "جواب برای | " .. tostring(matches) .. " | از لیست جواب های خودکار پاک شد.") + elseif text:match("^(پاسخگوی خودکار) (.*)$") then + local matches = text:match("^پاسخگوی خودکار (.*)$") + if matches == "روشن" then + redis:set("botBOT-IDautoanswer", true) + return send(msg.chat_id_, 0, "پاسخگویی خودکار تبلیغ گر فعال شد") + elseif matches == "خاموش" then + redis:del("botBOT-IDautoanswer") + return send(msg.chat_id_, 0, "حالت پاسخگویی خودکار تبلیغ گر غیر فعال شد.") + end + elseif text:match("^(تازه سازی)$")then + local list = {redis:smembers("botBOT-IDsupergroups"),redis:smembers("botBOT-IDgroups")} + tdcli_function({ + ID = "SearchContacts", + query_ = nil, + limit_ = 999999999 + }, function (i, naji) + redis:set("botBOT-IDcontacts", naji.total_count_) + end, nil) + for i, v in pairs(list) do + for a, b in pairs(v) do + tdcli_function ({ + ID = "GetChatMember", + chat_id_ = b, + user_id_ = bot_id + }, function (i,naji) + if naji.ID == "Error" then rem(i.id) + end + end, {id=b}) + end + end + return send(msg.chat_id_,msg.id_,"تازه‌سازی آمار تبلیغ‌گر شماره BOT-ID با موفقیت انجام شد.") + elseif text:match("^(وضعیت)$") then + local s = redis:get("botBOT-IDmaxjoin") and redis:ttl("botBOT-IDmaxjoin") or 0 + local ss = redis:get("botBOT-IDmaxlink") and redis:ttl("botBOT-IDmaxlink") or 0 + local msgadd = redis:get("botBOT-IDaddmsg") and "☑️" or "❎" + local numadd = redis:get("botBOT-IDaddcontact") and "✅" or "❎" + local txtadd = redis:get("botBOT-IDaddmsgtext") or "اد‌دی گلم خصوصی پیام بده" + local autoanswer = redis:get("botBOT-IDautoanswer") and "✅" or "❎" + local wlinks = redis:scard("botBOT-IDwaitelinks") + local glinks = redis:scard("botBOT-IDgoodlinks") + local links = redis:scard("botBOT-IDsavedlinks") + local txt = "⚙️ وضعیت اجرایی تبلیغ‌گر BOT-ID ⛓\n\n" .. tostring(autoanswer) .." حالت پاسخگویی خودکار 🗣 \n" .. tostring(numadd) .. " افزودن مخاطب با شماره 📞 \n" .. tostring(msgadd) .. " افزودن مخاطب با پیام 🗞\n〰〰〰ا〰〰〰\n📄 پیام افزودن مخاطب :\n📍 " .. tostring(txtadd) .. " 📍\n〰〰〰ا〰〰〰\n📁 لینک های ذخیره شده : " .. tostring(links) .. "\n⏲ لینک های در انتظار عضویت : " .. tostring(glinks) .. "\n🕖 " .. tostring(s) .. " ثانیه تا عضویت مجدد\n❄️ لینک های در انتظار تایید : " .. tostring(wlinks) .. "\n🕑️ " .. tostring(ss) .. " ثانیه تا تایید لینک مجدد\n 😼 سازنده : @i_naji" + return send(msg.chat_id_, 0, txt) + elseif text:match("^(امار)$") or text:match("^(آمار)$") then + local gps = redis:scard("botBOT-IDgroups") + local sgps = redis:scard("botBOT-IDsupergroups") + local usrs = redis:scard("botBOT-IDusers") + local links = redis:scard("botBOT-IDsavedlinks") + local glinks = redis:scard("botBOT-IDgoodlinks") + local wlinks = redis:scard("botBOT-IDwaitelinks") + tdcli_function({ + ID = "SearchContacts", + query_ = nil, + limit_ = 999999999 + }, function (i, naji) + redis:set("botBOT-IDcontacts", naji.total_count_) + end, nil) + local contacts = redis:get("botBOT-IDcontacts") + local text = [[ +📈 وضعیت و آمار تبلیغ گر 📊 + +👤 گفت و گو های شخصی : +]] .. tostring(usrs) .. [[ +👥 گروها : +]] .. tostring(gps) .. [[ +🌐 سوپر گروه ها : +]] .. tostring(sgps) .. [[ +📖 مخاطبین دخیره شده : +]] .. tostring(contacts)..[[ +📂 لینک های ذخیره شده : +]] .. tostring(links)..[[ + 😼 سازنده : @i_naji]] + return send(msg.chat_id_, 0, text) + elseif (text:match("^(ارسال به) (.*)$") and msg.reply_to_message_id_ ~= 0) then + local matches = text:match("^ارسال به (.*)$") + local naji + if matches:match("^(همه)$") then + naji = "botBOT-IDall" + elseif matches:match("^(خصوصی)") then + naji = "botBOT-IDusers" + elseif matches:match("^(گروه)$") then + naji = "botBOT-IDgroups" + elseif matches:match("^(سوپرگروه)$") then + naji = "botBOT-IDsupergroups" + else + return true + end + local list = redis:smembers(naji) + local id = msg.reply_to_message_id_ + for i, v in pairs(list) do + tdcli_function({ + ID = "ForwardMessages", + chat_id_ = v, + from_chat_id_ = msg.chat_id_, + message_ids_ = {[0] = id}, + disable_notification_ = 1, + from_background_ = 1 + }, dl_cb, nil) + end + return send(msg.chat_id_, msg.id_, "با موفقیت فرستاده شد") + elseif text:match("^(ارسال به سوپرگروه) (.*)") then + local matches = text:match("^ارسال به سوپرگروه (.*)") + local dir = redis:smembers("botBOT-IDsupergroups") + for i, v in pairs(dir) do + tdcli_function ({ + ID = "SendMessage", + chat_id_ = v, + reply_to_message_id_ = 0, + disable_notification_ = 0, + from_background_ = 1, + reply_markup_ = nil, + input_message_content_ = { + ID = "InputMessageText", + text_ = matches, + disable_web_page_preview_ = 1, + clear_draft_ = 0, + entities_ = {}, + parse_mode_ = nil + }, + }, dl_cb, nil) + end + return send(msg.chat_id_, msg.id_, "با موفقیت فرستاده شد") + elseif text:match("^(مسدودیت) (%d+)$") then + local matches = text:match("%d+") + rem(tonumber(matches)) + redis:sadd("botBOT-IDblockedusers",matches) + tdcli_function ({ + ID = "BlockUser", + user_id_ = tonumber(matches) + }, dl_cb, nil) + return send(msg.chat_id_, msg.id_, "کاربر مورد نظر مسدود شد") + elseif text:match("^(رفع مسدودیت) (%d+)$") then + local matches = text:match("%d+") + add(tonumber(matches)) + redis:srem("botBOT-IDblockedusers",matches) + tdcli_function ({ + ID = "UnblockUser", + user_id_ = tonumber(matches) + }, dl_cb, nil) + return send(msg.chat_id_, msg.id_, "مسدودیت کاربر مورد نظر رفع شد.") + elseif text:match('^(تنظیم نام) "(.*)" (.*)') then + local fname, lname = text:match('^تنظیم نام "(.*)" (.*)') + tdcli_function ({ + ID = "ChangeName", + first_name_ = fname, + last_name_ = lname + }, dl_cb, nil) + return send(msg.chat_id_, msg.id_, "نام جدید با موفقیت ثبت شد.") + elseif text:match("^(تنظیم نام کاربری) (.*)") then + local matches = text:match("^تنظیم نام کاربری (.*)") + tdcli_function ({ + ID = "ChangeUsername", + username_ = tostring(matches) + }, dl_cb, nil) + return send(msg.chat_id_, 0, 'تلاش برای تنظیم نام کاربری...') + elseif text:match("^(حذف نام کاربری)$") then + tdcli_function ({ + ID = "ChangeUsername", + username_ = "" + }, dl_cb, nil) + return send(msg.chat_id_, 0, 'نام کاربری با موفقیت حذف شد.') + elseif text:match('^(ارسال کن) "(.*)" (.*)') then + local id, txt = text:match('^ارسال کن "(.*)" (.*)') + send(id, 0, txt) + return send(msg.chat_id_, msg.id_, "ارسال شد") + elseif text:match("^(بگو) (.*)") then + local matches = text:match("^بگو (.*)") + return send(msg.chat_id_, 0, matches) + elseif text:match("^(شناسه من)$") then + return send(msg.chat_id_, msg.id_, "" .. msg.sender_user_id_ .."") + elseif text:match("^(ترک کردن) (.*)$") then + local matches = text:match("^ترک کردن (.*)$") + send(msg.chat_id_, msg.id_, 'تبلیغ‌گر از گروه مورد نظر خارج شد') + tdcli_function ({ + ID = "ChangeChatMemberStatus", + chat_id_ = matches, + user_id_ = bot_id, + status_ = {ID = "ChatMemberStatusLeft"}, + }, dl_cb, nil) + return rem(matches) + elseif text:match("^(افزودن به همه) (%d+)$") then + local matches = text:match("%d+") + local list = {redis:smembers("botBOT-IDgroups"),redis:smembers("botBOT-IDsupergroups")} + for a, b in pairs(list) do + for i, v in pairs(b) do + tdcli_function ({ + ID = "AddChatMember", + chat_id_ = v, + user_id_ = matches, + forward_limit_ = 50 + }, dl_cb, nil) + end + end + return send(msg.chat_id_, msg.id_, "کاربر مورد نظر به تمام گروه های من دعوت شد") + elseif (text:match("^(انلاین)$") and not msg.forward_info_)then + return tdcli_function({ + ID = "ForwardMessages", + chat_id_ = msg.chat_id_, + from_chat_id_ = msg.chat_id_, + message_ids_ = {[0] = msg.id_}, + disable_notification_ = 0, + from_background_ = 1 + }, dl_cb, nil) + elseif text:match("^(راهنما)$") then + return send(msg.chat_id_,msg.id_, "راهنمای تبلیغ‌گر : \nhttps://telegram.me/i_advertiser/15") + elseif tostring(msg.chat_id_):match("^-") then + if text:match("^(ترک کردن)$") then + rem(msg.chat_id_) + return tdcli_function ({ + ID = "ChangeChatMemberStatus", + chat_id_ = msg.chat_id_, + user_id_ = bot_id, + status_ = {ID = "ChatMemberStatusLeft"}, + }, dl_cb, nil) + elseif text:match("^(افزودن همه مخاطبین)$") then + tdcli_function({ + ID = "SearchContacts", + query_ = nil, + limit_ = 999999999 + },function(i, naji) + local users, count = redis:smembers("botBOT-IDusers"), naji.total_count_ + for n=0, tonumber(count) - 1 do + tdcli_function ({ + ID = "AddChatMember", + chat_id_ = i.chat_id, + user_id_ = naji.users_[n].id_, + forward_limit_ = 50 + }, dl_cb, nil) + end + for n=1, #users do + tdcli_function ({ + ID = "AddChatMember", + chat_id_ = i.chat_id, + user_id_ = users[n], + forward_limit_ = 50 + }, dl_cb, nil) + end + end, {chat_id=msg.chat_id_}) + return send(msg.chat_id_, msg.id_, "در حال افزودن مخاطبین به گروه ...") + end + end + end + if redis:sismember("botBOT-IDanswerslist", text) then + if redis:get("botBOT-IDautoanswer") then + if msg.sender_user_id_ ~= bot_id then + local answer = redis:hget("botBOT-IDanswers", text) + send(msg.chat_id_, 0, answer) + end + end + end + elseif msg.content_.ID == "MessageContact" then + local id = msg.content_.contact_.user_id_ + if not redis:sismember("botBOT-IDaddedcontacts",id) then + redis:sadd("botBOT-IDaddedcontacts",id) + local first = msg.content_.contact_.first_name_ or "-" + local last = msg.content_.contact_.last_name_ or "-" + local phone = msg.content_.contact_.phone_number_ + local id = msg.content_.contact_.user_id_ + tdcli_function ({ + ID = "ImportContacts", + contacts_ = {[0] = { + phone_number_ = tostring(phone), + first_name_ = tostring(first), + last_name_ = tostring(last), + user_id_ = id + }, + }, + }, dl_cb, nil) + if redis:get("botBOT-IDaddcontact") and msg.sender_user_id_ ~= bot_id then + local fname = redis:get("botBOT-IDfname") + local lnasme = redis:get("botBOT-IDlname") or "" + local num = redis:get("botBOT-IDnum") + tdcli_function ({ + ID = "SendMessage", + chat_id_ = msg.chat_id_, + reply_to_message_id_ = msg.id_, + disable_notification_ = 1, + from_background_ = 1, + reply_markup_ = nil, + input_message_content_ = { + ID = "InputMessageContact", + contact_ = { + ID = "Contact", + phone_number_ = num, + first_name_ = fname, + last_name_ = lname, + user_id_ = bot_id + }, + }, + }, dl_cb, nil) + end + end + if redis:get("botBOT-IDaddmsg") then + local answer = redis:get("botBOT-IDaddmsgtext") or "اددی گلم خصوصی پیام بده" + send(msg.chat_id_, msg.id_, answer) + end + elseif msg.content_.ID == "MessageChatDeleteMember" and msg.content_.id_ == bot_id then + return rem(msg.chat_id_) + elseif msg.content_.ID == "MessageChatJoinByLink" and msg.sender_user_id_ == bot_id then + return add(msg.chat_id_) + elseif msg.content_.ID == "MessageChatAddMembers" then + for i = 0, #msg.content_.members_ do + if msg.content_.members_[i].id_ == bot_id then + add(msg.chat_id_) + end + end + elseif msg.content_.caption_ then + return find_link(msg.content_.caption_) + end + if redis:get("botBOT-IDmarkread") then + tdcli_function ({ + ID = "ViewMessages", + chat_id_ = msg.chat_id_, + message_ids_ = {[0] = msg.id_} + }, dl_cb, nil) + end + elseif data.ID == "UpdateOption" and data.name_ == "my_id" then + tdcli_function ({ + ID = "GetChats", + offset_order_ = 9223372036854775807, + offset_chat_id_ = 0, + limit_ = 20 + }, dl_cb, nil) + end +end diff --git a/redis.lua b/redis.lua new file mode 100644 index 0000000..77aea79 --- /dev/null +++ b/redis.lua @@ -0,0 +1,1140 @@ +local redis = { + _VERSION = 'redis-lua 2.0.4', + _DESCRIPTION = 'A Lua client library for the redis key value storage system.', + _COPYRIGHT = 'Copyright (C) 2009-2012 Daniele Alessandri', +} + +-- The following line is used for backwards compatibility in order to keep the `Redis` +-- global module name. Using `Redis` is now deprecated so you should explicitly assign +-- the module to a local variable when requiring it: `local redis = require('redis')`. +Redis = redis + +local unpack = _G.unpack or table.unpack +local network, request, response = {}, {}, {} + +local defaults = { + host = '127.0.0.1', + port = 6379, + tcp_nodelay = true, + path = nil +} + +local function merge_defaults(parameters) + if parameters == nil then + parameters = {} + end + for k, v in pairs(defaults) do + if parameters[k] == nil then + parameters[k] = defaults[k] + end + end + return parameters +end + +local function parse_boolean(v) + if v == '1' or v == 'true' or v == 'TRUE' then + return true + elseif v == '0' or v == 'false' or v == 'FALSE' then + return false + else + return nil + end +end + +local function toboolean(value) return value == 1 end + +local function sort_request(client, command, key, params) + --[[ params = { + by = 'weight_*', + get = 'object_*', + limit = { 0, 10 }, + sort = 'desc', + alpha = true, + } ]] + local query = { key } + + if params then + if params.by then + table.insert(query, 'BY') + table.insert(query, params.by) + end + + if type(params.limit) == 'table' then + -- TODO: check for lower and upper limits + table.insert(query, 'LIMIT') + table.insert(query, params.limit[1]) + table.insert(query, params.limit[2]) + end + + if params.get then + if (type(params.get) == 'table') then + for _, getarg in pairs(params.get) do + table.insert(query, 'GET') + table.insert(query, getarg) + end + else + table.insert(query, 'GET') + table.insert(query, params.get) + end + end + + if params.sort then + table.insert(query, params.sort) + end + + if params.alpha == true then + table.insert(query, 'ALPHA') + end + + if params.store then + table.insert(query, 'STORE') + table.insert(query, params.store) + end + end + + request.multibulk(client, command, query) +end + +local function zset_range_request(client, command, ...) + local args, opts = {...}, { } + + if #args >= 1 and type(args[#args]) == 'table' then + local options = table.remove(args, #args) + if options.withscores then + table.insert(opts, 'WITHSCORES') + end + end + + for _, v in pairs(opts) do table.insert(args, v) end + request.multibulk(client, command, args) +end + +local function zset_range_byscore_request(client, command, ...) + local args, opts = {...}, { } + + if #args >= 1 and type(args[#args]) == 'table' then + local options = table.remove(args, #args) + if options.limit then + table.insert(opts, 'LIMIT') + table.insert(opts, options.limit.offset or options.limit[1]) + table.insert(opts, options.limit.count or options.limit[2]) + end + if options.withscores then + table.insert(opts, 'WITHSCORES') + end + end + + for _, v in pairs(opts) do table.insert(args, v) end + request.multibulk(client, command, args) +end + +local function zset_range_reply(reply, command, ...) + local args = {...} + local opts = args[4] + if opts and (opts.withscores or string.lower(tostring(opts)) == 'withscores') then + local new_reply = { } + for i = 1, #reply, 2 do + table.insert(new_reply, { reply[i], reply[i + 1] }) + end + return new_reply + else + return reply + end +end + +local function zset_store_request(client, command, ...) + local args, opts = {...}, { } + + if #args >= 1 and type(args[#args]) == 'table' then + local options = table.remove(args, #args) + if options.weights and type(options.weights) == 'table' then + table.insert(opts, 'WEIGHTS') + for _, weight in ipairs(options.weights) do + table.insert(opts, weight) + end + end + if options.aggregate then + table.insert(opts, 'AGGREGATE') + table.insert(opts, options.aggregate) + end + end + + for _, v in pairs(opts) do table.insert(args, v) end + request.multibulk(client, command, args) +end + +local function mset_filter_args(client, command, ...) + local args, arguments = {...}, {} + if (#args == 1 and type(args[1]) == 'table') then + for k,v in pairs(args[1]) do + table.insert(arguments, k) + table.insert(arguments, v) + end + else + arguments = args + end + request.multibulk(client, command, arguments) +end + +local function hash_multi_request_builder(builder_callback) + return function(client, command, ...) + local args, arguments = {...}, { } + if #args == 2 then + table.insert(arguments, args[1]) + for k, v in pairs(args[2]) do + builder_callback(arguments, k, v) + end + else + arguments = args + end + request.multibulk(client, command, arguments) + end +end + +local function parse_info(response) + local info = {} + local current = info + + response:gsub('([^\r\n]*)\r\n', function(kv) + if kv == '' then return end + + local section = kv:match('^# (%w+)$') + if section then + current = {} + info[section:lower()] = current + return + end + + local k,v = kv:match(('([^:]*):([^:]*)'):rep(1)) + if k:match('db%d+') then + current[k] = {} + v:gsub(',', function(dbkv) + local dbk,dbv = kv:match('([^:]*)=([^:]*)') + current[k][dbk] = dbv + end) + else + current[k] = v + end + end) + + return info +end + +local function load_methods(proto, commands) + local client = setmetatable ({}, getmetatable(proto)) + + for cmd, fn in pairs(commands) do + if type(fn) ~= 'function' then + redis.error('invalid type for command ' .. cmd .. '(must be a function)') + end + client[cmd] = fn + end + + for i, v in pairs(proto) do + client[i] = v + end + + return client +end + +local function create_client(proto, client_socket, commands) + local client = load_methods(proto, commands) + client.error = redis.error + client.network = { + socket = client_socket, + read = network.read, + write = network.write, + } + client.requests = { + multibulk = request.multibulk, + } + return client +end + +-- ############################################################################ + +function network.write(client, buffer) + local _, err = client.network.socket:send(buffer) + if err then client.error(err) end +end + +function network.read(client, len) + if len == nil then len = '*l' end + local line, err = client.network.socket:receive(len) + if not err then return line else client.error('connection error: ' .. err) end +end + +-- ############################################################################ + +function response.read(client) + local payload = client.network.read(client) + local prefix, data = payload:sub(1, -#payload), payload:sub(2) + + -- status reply + if prefix == '+' then + if data == 'OK' then + return true + elseif data == 'QUEUED' then + return { queued = true } + else + return data + end + + -- error reply + elseif prefix == '-' then + return client.error('redis error: ' .. data) + + -- integer reply + elseif prefix == ':' then + local number = tonumber(data) + + if not number then + if res == 'nil' then + return nil + end + client.error('cannot parse '..res..' as a numeric response.') + end + + return number + + -- bulk reply + elseif prefix == '$' then + local length = tonumber(data) + + if not length then + client.error('cannot parse ' .. length .. ' as data length') + end + + if length == -1 then + return nil + end + + local nextchunk = client.network.read(client, length + 2) + + return nextchunk:sub(1, -3) + + -- multibulk reply + elseif prefix == '*' then + local count = tonumber(data) + + if count == -1 then + return nil + end + + local list = {} + if count > 0 then + local reader = response.read + for i = 1, count do + list[i] = reader(client) + end + end + return list + + -- unknown type of reply + else + return client.error('unknown response prefix: ' .. prefix) + end +end + +-- ############################################################################ + +function request.raw(client, buffer) + local bufferType = type(buffer) + + if bufferType == 'table' then + client.network.write(client, table.concat(buffer)) + elseif bufferType == 'string' then + client.network.write(client, buffer) + else + client.error('argument error: ' .. bufferType) + end +end + +function request.multibulk(client, command, ...) + local args = {...} + local argsn = #args + local buffer = { true, true } + + if argsn == 1 and type(args[1]) == 'table' then + argsn, args = #args[1], args[1] + end + + buffer[1] = '*' .. tostring(argsn + 1) .. "\r\n" + buffer[2] = '$' .. #command .. "\r\n" .. command .. "\r\n" + + local table_insert = table.insert + for _, argument in pairs(args) do + local s_argument = tostring(argument) + table_insert(buffer, '$' .. #s_argument .. "\r\n" .. s_argument .. "\r\n") + end + + client.network.write(client, table.concat(buffer)) +end + +-- ############################################################################ + +local function custom(command, send, parse) + command = string.upper(command) + return function(client, ...) + send(client, command, ...) + local reply = response.read(client) + + if type(reply) == 'table' and reply.queued then + reply.parser = parse + return reply + else + if parse then + return parse(reply, command, ...) + end + return reply + end + end +end + +local function command(command, opts) + if opts == nil or type(opts) == 'function' then + return custom(command, request.multibulk, opts) + else + return custom(command, opts.request or request.multibulk, opts.response) + end +end + +local define_command_impl = function(target, name, opts) + local opts = opts or {} + target[string.lower(name)] = custom( + opts.command or string.upper(name), + opts.request or request.multibulk, + opts.response or nil + ) +end + +local undefine_command_impl = function(target, name) + target[string.lower(name)] = nil +end + +-- ############################################################################ + +local client_prototype = {} + +client_prototype.raw_cmd = function(client, buffer) + request.raw(client, buffer .. "\r\n") + return response.read(client) +end + +-- obsolete +client_prototype.define_command = function(client, name, opts) + define_command_impl(client, name, opts) +end + +-- obsolete +client_prototype.undefine_command = function(client, name) + undefine_command_impl(client, name) +end + +client_prototype.quit = function(client) + request.multibulk(client, 'QUIT') + client.network.socket:shutdown() + return true +end + +client_prototype.shutdown = function(client) + request.multibulk(client, 'SHUTDOWN') + client.network.socket:shutdown() +end + +-- Command pipelining + +client_prototype.pipeline = function(client, block) + local requests, replies, parsers = {}, {}, {} + local table_insert = table.insert + local socket_write, socket_read = client.network.write, client.network.read + + client.network.write = function(_, buffer) + table_insert(requests, buffer) + end + + -- TODO: this hack is necessary to temporarily reuse the current + -- request -> response handling implementation of redis-lua + -- without further changes in the code, but it will surely + -- disappear when the new command-definition infrastructure + -- will finally be in place. + client.network.read = function() return '+QUEUED' end + + local pipeline = setmetatable({}, { + __index = function(env, name) + local cmd = client[name] + if not cmd then + client.error('unknown redis command: ' .. name, 2) + end + return function(self, ...) + local reply = cmd(client, ...) + table_insert(parsers, #requests, reply.parser) + return reply + end + end + }) + + local success, retval = pcall(block, pipeline) + + client.network.write, client.network.read = socket_write, socket_read + if not success then client.error(retval, 0) end + + client.network.write(client, table.concat(requests, '')) + + for i = 1, #requests do + local reply, parser = response.read(client), parsers[i] + if parser then + reply = parser(reply) + end + table_insert(replies, i, reply) + end + + return replies, #requests +end + +-- Publish/Subscribe + +do + local channels = function(channels) + if type(channels) == 'string' then + channels = { channels } + end + return channels + end + + local subscribe = function(client, ...) + request.multibulk(client, 'subscribe', ...) + end + local psubscribe = function(client, ...) + request.multibulk(client, 'psubscribe', ...) + end + local unsubscribe = function(client, ...) + request.multibulk(client, 'unsubscribe') + end + local punsubscribe = function(client, ...) + request.multibulk(client, 'punsubscribe') + end + + local consumer_loop = function(client) + local aborting, subscriptions = false, 0 + + local abort = function() + if not aborting then + unsubscribe(client) + punsubscribe(client) + aborting = true + end + end + + return coroutine.wrap(function() + while true do + local message + local response = response.read(client) + + if response[1] == 'pmessage' then + message = { + kind = response[1], + pattern = response[2], + channel = response[3], + payload = response[4], + } + else + message = { + kind = response[1], + channel = response[2], + payload = response[3], + } + end + + if string.match(message.kind, '^p?subscribe$') then + subscriptions = subscriptions + 1 + end + if string.match(message.kind, '^p?unsubscribe$') then + subscriptions = subscriptions - 1 + end + + if aborting and subscriptions == 0 then + break + end + coroutine.yield(message, abort) + end + end) + end + + client_prototype.pubsub = function(client, subscriptions) + if type(subscriptions) == 'table' then + if subscriptions.subscribe then + subscribe(client, channels(subscriptions.subscribe)) + end + if subscriptions.psubscribe then + psubscribe(client, channels(subscriptions.psubscribe)) + end + end + return consumer_loop(client) + end +end + +-- Redis transactions (MULTI/EXEC) + +do + local function identity(...) return ... end + local emptytable = {} + + local function initialize_transaction(client, options, block, queued_parsers) + local table_insert = table.insert + local coro = coroutine.create(block) + + if options.watch then + local watch_keys = {} + for _, key in pairs(options.watch) do + table_insert(watch_keys, key) + end + if #watch_keys > 0 then + client:watch(unpack(watch_keys)) + end + end + + local transaction_client = setmetatable({}, {__index=client}) + transaction_client.exec = function(...) + client.error('cannot use EXEC inside a transaction block') + end + transaction_client.multi = function(...) + coroutine.yield() + end + transaction_client.commands_queued = function() + return #queued_parsers + end + + assert(coroutine.resume(coro, transaction_client)) + + transaction_client.multi = nil + transaction_client.discard = function(...) + local reply = client:discard() + for i, v in pairs(queued_parsers) do + queued_parsers[i]=nil + end + coro = initialize_transaction(client, options, block, queued_parsers) + return reply + end + transaction_client.watch = function(...) + client.error('WATCH inside MULTI is not allowed') + end + setmetatable(transaction_client, { __index = function(t, k) + local cmd = client[k] + if type(cmd) == "function" then + local function queuey(self, ...) + local reply = cmd(client, ...) + assert((reply or emptytable).queued == true, 'a QUEUED reply was expected') + table_insert(queued_parsers, reply.parser or identity) + return reply + end + t[k]=queuey + return queuey + else + return cmd + end + end + }) + client:multi() + return coro + end + + local function transaction(client, options, coroutine_block, attempts) + local queued_parsers, replies = {}, {} + local retry = tonumber(attempts) or tonumber(options.retry) or 2 + local coro = initialize_transaction(client, options, coroutine_block, queued_parsers) + + local success, retval + if coroutine.status(coro) == 'suspended' then + success, retval = coroutine.resume(coro) + else + -- do not fail if the coroutine has not been resumed (missing t:multi() with CAS) + success, retval = true, 'empty transaction' + end + if #queued_parsers == 0 or not success then + client:discard() + assert(success, retval) + return replies, 0 + end + + local raw_replies = client:exec() + if not raw_replies then + if (retry or 0) <= 0 then + client.error("MULTI/EXEC transaction aborted by the server") + else + --we're not quite done yet + return transaction(client, options, coroutine_block, retry - 1) + end + end + + local table_insert = table.insert + for i, parser in pairs(queued_parsers) do + table_insert(replies, i, parser(raw_replies[i])) + end + + return replies, #queued_parsers + end + + client_prototype.transaction = function(client, arg1, arg2) + local options, block + if not arg2 then + options, block = {}, arg1 + elseif arg1 then --and arg2, implicitly + options, block = type(arg1)=="table" and arg1 or { arg1 }, arg2 + else + client.error("Invalid parameters for redis transaction.") + end + + if not options.watch then + watch_keys = { } + for i, v in pairs(options) do + if tonumber(i) then + table.insert(watch_keys, v) + options[i] = nil + end + end + options.watch = watch_keys + elseif not (type(options.watch) == 'table') then + options.watch = { options.watch } + end + + if not options.cas then + local tx_block = block + block = function(client, ...) + client:multi() + return tx_block(client, ...) --can't wrap this in pcall because we're in a coroutine. + end + end + + return transaction(client, options, block) + end +end + +-- MONITOR context + +do + local monitor_loop = function(client) + local monitoring = true + + -- Tricky since the payload format changed starting from Redis 2.6. + local pattern = '^(%d+%.%d+)( ?.- ?) ?"(%a+)" ?(.-)$' + + local abort = function() + monitoring = false + end + + return coroutine.wrap(function() + client:monitor() + + while monitoring do + local message, matched + local response = response.read(client) + + local ok = response:gsub(pattern, function(time, info, cmd, args) + message = { + timestamp = tonumber(time), + client = info:match('%d+.%d+.%d+.%d+:%d+'), + database = tonumber(info:match('%d+')) or 0, + command = cmd, + arguments = args:match('.+'), + } + matched = true + end) + + if not matched then + client.error('Unable to match MONITOR payload: '..response) + end + + coroutine.yield(message, abort) + end + end) + end + + client_prototype.monitor_messages = function(client) + return monitor_loop(client) + end +end + +-- ############################################################################ + +local function connect_tcp(socket, parameters) + local host, port = parameters.host, tonumber(parameters.port) + local ok, err = socket:connect(host, port) + if not ok then + redis.error('could not connect to '..host..':'..port..' ['..err..']') + end + socket:setoption('tcp-nodelay', parameters.tcp_nodelay) + return socket +end + +local function connect_unix(socket, parameters) + local ok, err = socket:connect(parameters.path) + if not ok then + redis.error('could not connect to '..parameters.path..' ['..err..']') + end + return socket +end + +local function create_connection(parameters) + if parameters.socket then + return parameters.socket + end + + local perform_connection, socket + + if parameters.scheme == 'unix' then + perform_connection, socket = connect_unix, require('socket.unix') + assert(socket, 'your build of LuaSocket does not support UNIX domain sockets') + else + if parameters.scheme then + local scheme = parameters.scheme + assert(scheme == 'redis' or scheme == 'tcp', 'invalid scheme: '..scheme) + end + perform_connection, socket = connect_tcp, require('socket').tcp + end + + return perform_connection(socket(), parameters) +end + +-- ############################################################################ + +function redis.error(message, level) + error(message, (level or 1) + 1) +end + +function redis.connect(...) + local args, parameters = {...}, nil + + if #args == 1 then + if type(args[1]) == 'table' then + parameters = args[1] + else + local uri = require('socket.url') + parameters = uri.parse(select(1, ...)) + if parameters.scheme then + if parameters.query then + for k, v in parameters.query:gmatch('([-_%w]+)=([-_%w]+)') do + if k == 'tcp_nodelay' or k == 'tcp-nodelay' then + parameters.tcp_nodelay = parse_boolean(v) + end + end + end + else + parameters.host = parameters.path + end + end + elseif #args > 1 then + local host, port = unpack(args) + parameters = { host = host, port = port } + end + + local commands = redis.commands or {} + if type(commands) ~= 'table' then + redis.error('invalid type for the commands table') + end + + local socket = create_connection(merge_defaults(parameters)) + local client = create_client(client_prototype, socket, commands) + + return client +end + +function redis.command(cmd, opts) + return command(cmd, opts) +end + +-- obsolete +function redis.define_command(name, opts) + define_command_impl(redis.commands, name, opts) +end + +-- obsolete +function redis.undefine_command(name) + undefine_command_impl(redis.commands, name) +end + +-- ############################################################################ + +-- Commands defined in this table do not take the precedence over +-- methods defined in the client prototype table. + +redis.commands = { + -- commands operating on the key space + exists = command('EXISTS', { + response = toboolean + }), + del = command('DEL'), + type = command('TYPE'), + rename = command('RENAME'), + renamenx = command('RENAMENX', { + response = toboolean + }), + expire = command('EXPIRE', { + response = toboolean + }), + pexpire = command('PEXPIRE', { -- >= 2.6 + response = toboolean + }), + expireat = command('EXPIREAT', { + response = toboolean + }), + pexpireat = command('PEXPIREAT', { -- >= 2.6 + response = toboolean + }), + ttl = command('TTL'), + pttl = command('PTTL'), -- >= 2.6 + move = command('MOVE', { + response = toboolean + }), + dbsize = command('DBSIZE'), + persist = command('PERSIST', { -- >= 2.2 + response = toboolean + }), + keys = command('KEYS', { + response = function(response) + if type(response) == 'string' then + -- backwards compatibility path for Redis < 2.0 + local keys = {} + response:gsub('[^%s]+', function(key) + table.insert(keys, key) + end) + response = keys + end + return response + end + }), + randomkey = command('RANDOMKEY', { + response = function(response) + if response == '' then + return nil + else + return response + end + end + }), + sort = command('SORT', { + request = sort_request, + }), + + -- commands operating on string values + set = command('SET'), + setnx = command('SETNX', { + response = toboolean + }), + setex = command('SETEX'), -- >= 2.0 + psetex = command('PSETEX'), -- >= 2.6 + mset = command('MSET', { + request = mset_filter_args + }), + msetnx = command('MSETNX', { + request = mset_filter_args, + response = toboolean + }), + get = command('GET'), + mget = command('MGET'), + getset = command('GETSET'), + incr = command('INCR'), + incrby = command('INCRBY'), + incrbyfloat = command('INCRBYFLOAT', { -- >= 2.6 + response = function(reply, command, ...) + return tonumber(reply) + end, + }), + decr = command('DECR'), + decrby = command('DECRBY'), + append = command('APPEND'), -- >= 2.0 + substr = command('SUBSTR'), -- >= 2.0 + strlen = command('STRLEN'), -- >= 2.2 + setrange = command('SETRANGE'), -- >= 2.2 + getrange = command('GETRANGE'), -- >= 2.2 + setbit = command('SETBIT'), -- >= 2.2 + getbit = command('GETBIT'), -- >= 2.2 + + -- commands operating on lists + rpush = command('RPUSH'), + lpush = command('LPUSH'), + llen = command('LLEN'), + lrange = command('LRANGE'), + ltrim = command('LTRIM'), + lindex = command('LINDEX'), + lset = command('LSET'), + lrem = command('LREM'), + lpop = command('LPOP'), + rpop = command('RPOP'), + rpoplpush = command('RPOPLPUSH'), + blpop = command('BLPOP'), -- >= 2.0 + brpop = command('BRPOP'), -- >= 2.0 + rpushx = command('RPUSHX'), -- >= 2.2 + lpushx = command('LPUSHX'), -- >= 2.2 + linsert = command('LINSERT'), -- >= 2.2 + brpoplpush = command('BRPOPLPUSH'), -- >= 2.2 + + -- commands operating on sets + sadd = command('SADD'), + srem = command('SREM'), + spop = command('SPOP'), + smove = command('SMOVE', { + response = toboolean + }), + scard = command('SCARD'), + sismember = command('SISMEMBER', { + response = toboolean + }), + sinter = command('SINTER'), + sinterstore = command('SINTERSTORE'), + sunion = command('SUNION'), + sunionstore = command('SUNIONSTORE'), + sdiff = command('SDIFF'), + sdiffstore = command('SDIFFSTORE'), + smembers = command('SMEMBERS'), + srandmember = command('SRANDMEMBER'), + + -- commands operating on sorted sets + zadd = command('ZADD'), + zincrby = command('ZINCRBY'), + zrem = command('ZREM'), + zrange = command('ZRANGE', { + request = zset_range_request, + response = zset_range_reply, + }), + zrevrange = command('ZREVRANGE', { + request = zset_range_request, + response = zset_range_reply, + }), + zrangebyscore = command('ZRANGEBYSCORE', { + request = zset_range_byscore_request, + response = zset_range_reply, + }), + zrevrangebyscore = command('ZREVRANGEBYSCORE', { -- >= 2.2 + request = zset_range_byscore_request, + response = zset_range_reply, + }), + zunionstore = command('ZUNIONSTORE', { -- >= 2.0 + request = zset_store_request + }), + zinterstore = command('ZINTERSTORE', { -- >= 2.0 + request = zset_store_request + }), + zcount = command('ZCOUNT'), + zcard = command('ZCARD'), + zscore = command('ZSCORE'), + zremrangebyscore = command('ZREMRANGEBYSCORE'), + zrank = command('ZRANK'), -- >= 2.0 + zrevrank = command('ZREVRANK'), -- >= 2.0 + zremrangebyrank = command('ZREMRANGEBYRANK'), -- >= 2.0 + + -- commands operating on hashes + hset = command('HSET', { -- >= 2.0 + response = toboolean + }), + hsetnx = command('HSETNX', { -- >= 2.0 + response = toboolean + }), + hmset = command('HMSET', { -- >= 2.0 + request = hash_multi_request_builder(function(args, k, v) + table.insert(args, k) + table.insert(args, v) + end), + }), + hincrby = command('HINCRBY'), -- >= 2.0 + hincrbyfloat = command('HINCRBYFLOAT', {-- >= 2.6 + response = function(reply, command, ...) + return tonumber(reply) + end, + }), + hget = command('HGET'), -- >= 2.0 + hmget = command('HMGET', { -- >= 2.0 + request = hash_multi_request_builder(function(args, k, v) + table.insert(args, v) + end), + }), + hdel = command('HDEL'), -- >= 2.0 + hexists = command('HEXISTS', { -- >= 2.0 + response = toboolean + }), + hlen = command('HLEN'), -- >= 2.0 + hkeys = command('HKEYS'), -- >= 2.0 + hvals = command('HVALS'), -- >= 2.0 + hgetall = command('HGETALL', { -- >= 2.0 + response = function(reply, command, ...) + local new_reply = { } + for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end + return new_reply + end + }), + + -- connection related commands + ping = command('PING', { + response = function(response) return response == 'PONG' end + }), + echo = command('ECHO'), + auth = command('AUTH'), + select = command('SELECT'), + + -- transactions + multi = command('MULTI'), -- >= 2.0 + exec = command('EXEC'), -- >= 2.0 + discard = command('DISCARD'), -- >= 2.0 + watch = command('WATCH'), -- >= 2.2 + unwatch = command('UNWATCH'), -- >= 2.2 + + -- publish - subscribe + subscribe = command('SUBSCRIBE'), -- >= 2.0 + unsubscribe = command('UNSUBSCRIBE'), -- >= 2.0 + psubscribe = command('PSUBSCRIBE'), -- >= 2.0 + punsubscribe = command('PUNSUBSCRIBE'), -- >= 2.0 + publish = command('PUBLISH'), -- >= 2.0 + + -- redis scripting + eval = command('EVAL'), -- >= 2.6 + evalsha = command('EVALSHA'), -- >= 2.6 + script = command('SCRIPT'), -- >= 2.6 + + -- remote server control commands + bgrewriteaof = command('BGREWRITEAOF'), + config = command('CONFIG', { -- >= 2.0 + response = function(reply, command, ...) + if (type(reply) == 'table') then + local new_reply = { } + for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end + return new_reply + end + + return reply + end + }), + client = command('CLIENT'), -- >= 2.4 + slaveof = command('SLAVEOF'), + save = command('SAVE'), + bgsave = command('BGSAVE'), + lastsave = command('LASTSAVE'), + flushdb = command('FLUSHDB'), + flushall = command('FLUSHALL'), + monitor = command('MONITOR'), + time = command('TIME'), -- >= 2.6 + slowlog = command('SLOWLOG', { -- >= 2.2.13 + response = function(reply, command, ...) + if (type(reply) == 'table') then + local structured = { } + for index, entry in ipairs(reply) do + structured[index] = { + id = tonumber(entry[1]), + timestamp = tonumber(entry[2]), + duration = tonumber(entry[3]), + command = entry[4], + } + end + return structured + end + + return reply + end + }), + info = command('INFO', { + response = parse_info, + }), +} + +-- ############################################################################ + +return redis