├── .README.MD_images └── 1be8fb97.png ├── README.MD ├── _bot.py ├── bot.py ├── config.json ├── main.py ├── modules ├── __init__.py ├── account_detail.py ├── add_account.py ├── batch_test_accounts.py ├── batch_test_delete_accounts.py ├── create_droplet.py ├── delete_account.py ├── droplet_actions.py ├── droplet_detail.py ├── list_droplets.py ├── manage_accounts.py ├── manage_droplets.py └── start.py ├── requirements.txt └── utils ├── db.py ├── localizer.py ├── password_generator.py └── set_root_password_script.py /.README.MD_images/1be8fb97.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realByg/digitalocean-helper-bot/3849c231a931f4f01017a612a94134a5e047b3c1/.README.MD_images/1be8fb97.png -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # ☁ `digitalocean-helper-bot` 2 | 3 | ![](.README.MD_images/1be8fb97.png) 4 | 5 | ### 🚚 用 telegram bot 管理 Digital Ocean 账号 6 | 7 | ### 🔖 主要功能 8 | 9 | + 管理 Digital Ocean 账号,查看账号余额,批量测试账号 10 | + 创建和管理虚拟机 11 | 12 | ### 🪂 环境 13 | 14 | ``` 15 | python3 16 | ``` 17 | 18 | ### 😻 用法 19 | 20 | 1. `git clone` 项目至本地,`pip3 install -r requirements.txt` 安装依赖 21 | 2. 在 [@botfather](https://t.me/botfather) 新建一个 `bot`,记下 `token` 22 | 3. 编辑 `config.json`,填入相关配置 23 | 4. `python3 main.py` 运行 `bot` -------------------------------------------------------------------------------- /_bot.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | import telebot 4 | 5 | bot_token = environ.get('bot_token') 6 | 7 | bot = telebot.TeleBot(token=bot_token) 8 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import traceback 4 | from os import environ 5 | from typing import Union 6 | import urllib.parse as urlparse 7 | from urllib.parse import parse_qs 8 | 9 | import telebot 10 | from telebot.types import CallbackQuery, Message 11 | 12 | from _bot import bot 13 | # noinspection PyUnresolvedReferences 14 | from modules import * 15 | 16 | bot_admins = json.loads(environ.get('bot_admins')) 17 | 18 | logger = telebot.logger 19 | telebot.logger.setLevel(logging.INFO) 20 | 21 | command_dict = { 22 | '/start': 'start', 23 | 24 | '/aa': 'add_account', 25 | '/ma': 'manage_accounts', 26 | '/bta': 'batch_test_accounts', 27 | 28 | '/cd': 'create_droplet', 29 | '/md': 'manage_droplets', 30 | } 31 | 32 | 33 | @bot.message_handler(content_types=['text']) 34 | def text_handler(m: Message): 35 | try: 36 | logger.info(m) 37 | 38 | if m.from_user.id not in bot_admins: 39 | return 40 | 41 | if m.text in command_dict.keys(): 42 | globals()[command_dict[m.text]](m) 43 | 44 | except Exception as e: 45 | traceback.print_exc() 46 | handle_exception(m, e) 47 | 48 | 49 | @bot.callback_query_handler(func=lambda call: True) 50 | def callback_query_handler(call: CallbackQuery): 51 | try: 52 | logger.info(call) 53 | 54 | if call.from_user.id not in bot_admins: 55 | return 56 | 57 | callback_data = urlparse.urlparse(call.data) 58 | func_name = callback_data.path 59 | data = parse_qs(callback_data.query) 60 | if func_name in globals(): 61 | args = [call] 62 | if len(data.keys()) > 0: 63 | args.append(data) 64 | 65 | globals()[func_name](*args) 66 | 67 | except Exception as e: 68 | traceback.print_exc() 69 | handle_exception(call, e) 70 | 71 | 72 | def handle_exception(d: Union[Message, CallbackQuery], e): 73 | bot.send_message( 74 | text=f'出错啦\n' 75 | f'{e}', 76 | chat_id=d.from_user.id, 77 | parse_mode='HTML' 78 | ) 79 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "BOT": { 3 | "NAME": "DigitalOcean 小助手", 4 | "TOKEN": "", 5 | "ADMINS": [ 6 | ] 7 | } 8 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | def parse_config(): 2 | import json 3 | from os import environ 4 | 5 | config = json.load(open('config.json', 'r', encoding='utf-8')) 6 | environ['bot_name'] = config['BOT']['NAME'] 7 | environ['bot_token'] = config['BOT']['TOKEN'] 8 | environ['bot_admins'] = json.dumps(config['BOT']['ADMINS']) 9 | 10 | 11 | def start_bot(): 12 | from bot import bot 13 | 14 | bot.polling(none_stop=True) 15 | 16 | 17 | if __name__ == '__main__': 18 | parse_config() 19 | start_bot() 20 | -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- 1 | from .start import start 2 | from .add_account import add_account 3 | from .manage_accounts import manage_accounts 4 | from .batch_test_accounts import batch_test_accounts 5 | from .account_detail import account_detail 6 | from .manage_droplets import manage_droplets 7 | from .delete_account import delete_account 8 | from .batch_test_delete_accounts import batch_test_delete_accounts 9 | from .create_droplet import create_droplet 10 | from .list_droplets import list_droplets 11 | from .droplet_detail import droplet_detail 12 | from .droplet_actions import droplet_actions 13 | -------------------------------------------------------------------------------- /modules/account_detail.py: -------------------------------------------------------------------------------- 1 | from telebot.types import ( 2 | CallbackQuery, 3 | InlineKeyboardMarkup, 4 | InlineKeyboardButton, 5 | ) 6 | 7 | import digitalocean 8 | from digitalocean import DataReadError 9 | 10 | from _bot import bot 11 | from utils.db import AccountsDB 12 | 13 | 14 | def account_detail(call: CallbackQuery, data: dict): 15 | doc_id = data['doc_id'][0] 16 | t = '账号信息\n\n' 17 | 18 | account = AccountsDB().get(doc_id=doc_id) 19 | 20 | msg = bot.send_message( 21 | text=f'{t}' 22 | f'邮箱: {account["email"]}\n\n' 23 | f'获取信息中...', 24 | chat_id=call.from_user.id, 25 | parse_mode='HTML' 26 | ) 27 | 28 | t += f'邮箱: {account["email"]}\n' \ 29 | f'备注: {account["remarks"]}\n' \ 30 | f'添加日期: {account["date"]}\n' \ 31 | f'Token: {account["token"]}\n\n' 32 | markup = InlineKeyboardMarkup() 33 | markup.row( 34 | InlineKeyboardButton( 35 | text='删除', 36 | callback_data=f'delete_account?doc_id={account.doc_id}' 37 | ) 38 | ) 39 | 40 | try: 41 | account_balance = digitalocean.Balance().get_object(api_token=account['token']) 42 | 43 | t += f'账户余额: {account_balance.account_balance}\n' \ 44 | f'账单已用: {account_balance.month_to_date_usage}\n' \ 45 | f'账单日期: {account_balance.generated_at.split("T")[0]}' 46 | 47 | except DataReadError as e: 48 | t += f'获取账单错误: {e}' 49 | 50 | bot.edit_message_text( 51 | text=t, 52 | chat_id=call.from_user.id, 53 | message_id=msg.message_id, 54 | parse_mode='HTML', 55 | reply_markup=markup 56 | ) 57 | -------------------------------------------------------------------------------- /modules/add_account.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from telebot.types import ( 4 | Message, 5 | CallbackQuery 6 | ) 7 | 8 | import digitalocean 9 | from digitalocean import DataReadError 10 | 11 | from _bot import bot 12 | from utils.db import AccountsDB 13 | from .start import start 14 | 15 | 16 | def add_account(d: Union[Message, CallbackQuery]): 17 | t = '添加账号\n\n' \ 18 | '请回复账号 Token 和备注,一行一个并用冒号隔开\n\n' \ 19 | '示例:\n' \ 20 | 'token123:备注xxx\n' \ 21 | 'token345\n\n' \ 22 | '/cancel 取消' 23 | 24 | msg = bot.send_message( 25 | text=t, 26 | chat_id=d.from_user.id, 27 | parse_mode='HTML', 28 | disable_web_page_preview=True 29 | ) 30 | 31 | bot.register_next_step_handler(msg, add_account_next_step_handler) 32 | 33 | 34 | def add_account_next_step_handler(m: Message): 35 | if m.text == '/cancel': 36 | start(m) 37 | return 38 | 39 | msg = bot.send_message( 40 | text='添加账号中...', 41 | chat_id=m.from_user.id 42 | ) 43 | 44 | accounts = m.text.split('\n') 45 | added_accounts = [] 46 | failed_accounts = [] 47 | 48 | for account in accounts: 49 | if ':' in account: 50 | token = account.split(':')[0] 51 | remarks = account.split(':')[1] 52 | else: 53 | token = account 54 | remarks = '' 55 | 56 | try: 57 | email = digitalocean.Account().get_object( 58 | api_token=token 59 | ).email 60 | 61 | AccountsDB().save( 62 | email=email, 63 | token=token, 64 | remarks=remarks 65 | ) 66 | 67 | added_accounts.append(email) 68 | 69 | except DataReadError: 70 | failed_accounts.append(account) 71 | 72 | t = f'共 {len(accounts)} 个账号\n\n' 73 | 74 | if added_accounts: 75 | t += f'添加成功 {len(added_accounts)} 个:\n' 76 | for added_account in added_accounts: 77 | t += f'{added_account}\n' 78 | t += '\n' 79 | 80 | if failed_accounts: 81 | t += f'添加失败 {len(failed_accounts)} 个:\n' 82 | for failed_account in failed_accounts: 83 | t += f'{failed_account}\n' 84 | 85 | bot.edit_message_text( 86 | text=t, 87 | chat_id=m.from_user.id, 88 | message_id=msg.message_id, 89 | parse_mode='HTML' 90 | ) 91 | -------------------------------------------------------------------------------- /modules/batch_test_accounts.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Union 3 | 4 | from telebot.types import ( 5 | Message, 6 | CallbackQuery, 7 | InlineKeyboardMarkup, 8 | InlineKeyboardButton, 9 | ) 10 | 11 | import digitalocean 12 | from digitalocean import DataReadError 13 | 14 | from _bot import bot 15 | from utils.db import AccountsDB 16 | 17 | 18 | def batch_test_accounts(d: Union[Message, CallbackQuery]): 19 | t = '批量测试账号\n\n' 20 | markup = InlineKeyboardMarkup() 21 | 22 | msg = bot.send_message( 23 | text=f'{t}' 24 | f'测试中...', 25 | chat_id=d.from_user.id, 26 | parse_mode='HTML', 27 | ) 28 | 29 | accounts = AccountsDB().all() 30 | checked_accounts = [] 31 | failed_accounts = [] 32 | 33 | for account in accounts: 34 | try: 35 | account_balance = digitalocean.Balance().get_object(api_token=account['token']) 36 | account_balance.email = account['email'] 37 | 38 | checked_accounts.append(account_balance) 39 | 40 | except DataReadError: 41 | failed_accounts.append(account['email']) 42 | 43 | t += f'共 {len(accounts)} 个账号\n\n' 44 | 45 | if checked_accounts: 46 | t += f'测试成功 {len(checked_accounts)} 个:\n' 47 | for account_balance in checked_accounts: 48 | t += f'{account_balance.email} | {account_balance.account_balance}\n' 49 | t += '\n' 50 | 51 | if failed_accounts: 52 | t += f'测试失败 {len(failed_accounts)} 个:\n' 53 | for email in failed_accounts: 54 | t += f'{email}\n' 55 | markup.add( 56 | InlineKeyboardButton( 57 | text='删除失败账号', 58 | callback_data=json.dumps({ 59 | 't': 'batch_test_delete_accounts' 60 | }) 61 | ) 62 | ) 63 | 64 | bot.edit_message_text( 65 | text=t, 66 | chat_id=d.from_user.id, 67 | message_id=msg.message_id, 68 | parse_mode='HTML', 69 | reply_markup=markup 70 | ) 71 | -------------------------------------------------------------------------------- /modules/batch_test_delete_accounts.py: -------------------------------------------------------------------------------- 1 | from telebot.types import CallbackQuery 2 | 3 | import digitalocean 4 | from digitalocean import DataReadError 5 | 6 | from _bot import bot 7 | from utils.db import AccountsDB 8 | 9 | 10 | def batch_test_delete_accounts(call: CallbackQuery): 11 | bot.edit_message_text( 12 | text=f'{call.message.html_text}\n\n' 13 | f'删除失败账号中...', 14 | chat_id=call.from_user.id, 15 | message_id=call.message.message_id, 16 | parse_mode='HTML' 17 | ) 18 | 19 | accounts_db = AccountsDB() 20 | 21 | accounts = accounts_db.all() 22 | for account in accounts: 23 | try: 24 | digitalocean.Balance().get_object(api_token=account['token']) 25 | 26 | except DataReadError: 27 | accounts_db.remove(doc_id=account.doc_id) 28 | 29 | bot.edit_message_text( 30 | text=f'{call.message.html_text}\n\n' 31 | f'已删除失败账号', 32 | chat_id=call.from_user.id, 33 | message_id=call.message.message_id, 34 | parse_mode='HTML' 35 | ) 36 | -------------------------------------------------------------------------------- /modules/create_droplet.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from time import sleep 3 | 4 | from telebot.types import ( 5 | Message, 6 | CallbackQuery, 7 | InlineKeyboardMarkup, 8 | InlineKeyboardButton, 9 | ) 10 | 11 | import digitalocean 12 | 13 | from _bot import bot 14 | from utils.db import AccountsDB 15 | from utils.localizer import localize_region 16 | from utils.set_root_password_script import set_root_password_script 17 | from utils.password_generator import password_generator 18 | 19 | user_dict = {} 20 | 21 | t = '创建实例\n\n' 22 | 23 | 24 | def create_droplet(d: Union[Message, CallbackQuery], data: dict = None): 25 | data = data or {} 26 | next_func = data.get('nf', ['select_account'])[0] 27 | if next_func in globals(): 28 | data.pop('nf', None) 29 | args = [d] 30 | if len(data.keys()) > 0: 31 | args.append(data) 32 | 33 | globals()[next_func](*args) 34 | 35 | 36 | def select_account(d: Union[Message, CallbackQuery]): 37 | accounts = AccountsDB().all() 38 | markup = InlineKeyboardMarkup() 39 | for account in accounts: 40 | markup.add( 41 | InlineKeyboardButton( 42 | text=account['email'], 43 | callback_data=f'create_droplet?nf=select_region&doc_id={account.doc_id}' 44 | ) 45 | ) 46 | 47 | bot.send_message( 48 | text=f'{t}' 49 | '请选择账号', 50 | chat_id=d.from_user.id, 51 | parse_mode='HTML', 52 | reply_markup=markup 53 | ) 54 | 55 | 56 | def select_region(call: CallbackQuery, data: dict): 57 | doc_id = data['doc_id'][0] 58 | 59 | account = AccountsDB().get(doc_id=doc_id) 60 | user_dict[call.from_user.id] = { 61 | 'account': account 62 | } 63 | 64 | _t = t + f'账号: {account["email"]}\n\n' 65 | 66 | bot.edit_message_text( 67 | text=f'{_t}' 68 | f'获取地区中...', 69 | chat_id=call.from_user.id, 70 | message_id=call.message.message_id, 71 | parse_mode='HTML' 72 | ) 73 | 74 | regions = digitalocean.Manager(token=account['token']).get_all_regions() 75 | 76 | markup = InlineKeyboardMarkup(row_width=2) 77 | buttons = [] 78 | for region in regions: 79 | if region.available: 80 | buttons.append( 81 | InlineKeyboardButton( 82 | text=localize_region(slug=region.slug), 83 | callback_data=f'create_droplet?nf=select_size®ion={region.slug}' 84 | ) 85 | ) 86 | markup.add(*buttons) 87 | 88 | bot.edit_message_text( 89 | text=f'{_t}' 90 | f'请选则地区', 91 | chat_id=call.from_user.id, 92 | message_id=call.message.message_id, 93 | reply_markup=markup, 94 | parse_mode='HTML' 95 | ) 96 | 97 | 98 | def select_size(call: CallbackQuery, data: dict): 99 | region_slug = data['region'][0] 100 | 101 | user_dict[call.from_user.id].update({ 102 | 'region_slug': region_slug 103 | }) 104 | 105 | _t = t + f'账号: {user_dict[call.from_user.id]["account"]["email"]}\n' \ 106 | f'地区: {region_slug}\n\n' 107 | 108 | bot.edit_message_text( 109 | text=f'{_t}' 110 | f'获取型号中...', 111 | chat_id=call.from_user.id, 112 | message_id=call.message.message_id, 113 | parse_mode='HTML' 114 | ) 115 | 116 | sizes = digitalocean.Manager(token=user_dict[call.from_user.id]['account']['token']).get_all_sizes() 117 | 118 | markup = InlineKeyboardMarkup(row_width=2) 119 | buttons = [] 120 | for size in sizes: 121 | if region_slug in size.regions: 122 | buttons.append( 123 | InlineKeyboardButton( 124 | text=size.slug, 125 | callback_data=f'create_droplet?nf=select_image&size={size.slug}' 126 | ) 127 | ) 128 | markup.add(*buttons) 129 | markup.row( 130 | InlineKeyboardButton( 131 | text='上一步', 132 | callback_data=f'create_droplet?nf=select_region&doc_id={user_dict[call.from_user.id]["account"].doc_id}' 133 | ) 134 | ) 135 | 136 | bot.edit_message_text( 137 | text=f'{_t}' 138 | f'请选则型号', 139 | chat_id=call.from_user.id, 140 | message_id=call.message.message_id, 141 | reply_markup=markup, 142 | parse_mode='HTML' 143 | ) 144 | 145 | 146 | def select_image(d: Union[Message, CallbackQuery], data: dict): 147 | size_slug = data['size'][0] 148 | 149 | user_dict[d.from_user.id].update({ 150 | 'size_slug': size_slug 151 | }) 152 | 153 | _t = t + f'账号: {user_dict[d.from_user.id]["account"]["email"]}\n' \ 154 | f'地区: {user_dict[d.from_user.id]["region_slug"]}\n' \ 155 | f'型号: {size_slug}\n\n' 156 | 157 | def get_image_markup(): 158 | images = digitalocean.Manager(token=user_dict[d.from_user.id]['account']['token']).get_distro_images() 159 | 160 | markup = InlineKeyboardMarkup(row_width=2) 161 | buttons = [] 162 | for image in images: 163 | if image.distribution in ['Ubuntu', 'CentOS', 'Debian'] \ 164 | and image.public \ 165 | and image.status == 'available' \ 166 | and user_dict[d.from_user.id]["region_slug"] in image.regions: 167 | buttons.append( 168 | InlineKeyboardButton( 169 | text=f'{image.distribution} {image.name}', 170 | callback_data=f'create_droplet?nf=get_name&image={image.slug}' 171 | ) 172 | ) 173 | markup.add(*buttons) 174 | markup.row( 175 | InlineKeyboardButton( 176 | text='上一步', 177 | callback_data=f'create_droplet?nf=select_size®ion={user_dict[d.from_user.id]["region_slug"]}' 178 | ) 179 | ) 180 | 181 | return markup 182 | 183 | if type(d) == Message: 184 | msg = bot.send_message( 185 | text=f'{_t}' 186 | f'获取镜像中...', 187 | chat_id=d.from_user.id, 188 | parse_mode='HTML' 189 | ) 190 | bot.edit_message_text( 191 | text=f'{_t}' 192 | f'请选则镜像', 193 | chat_id=d.from_user.id, 194 | message_id=msg.message_id, 195 | reply_markup=get_image_markup(), 196 | parse_mode='HTML' 197 | ) 198 | 199 | elif type(d) == CallbackQuery: 200 | bot.edit_message_text( 201 | text=f'{_t}' 202 | f'获取镜像中...', 203 | chat_id=d.from_user.id, 204 | message_id=d.message.message_id, 205 | parse_mode='HTML' 206 | ) 207 | bot.edit_message_text( 208 | text=f'{_t}' 209 | f'请选则镜像', 210 | chat_id=d.from_user.id, 211 | message_id=d.message.message_id, 212 | reply_markup=get_image_markup(), 213 | parse_mode='HTML' 214 | ) 215 | 216 | 217 | def get_name(call: CallbackQuery, data: dict): 218 | image_slug = data['image'][0] 219 | 220 | user_dict[call.from_user.id].update({ 221 | 'image_slug': image_slug 222 | }) 223 | 224 | _t = t + f'账号: {user_dict[call.from_user.id]["account"]["email"]}\n' \ 225 | f'地区: {user_dict[call.from_user.id]["region_slug"]}\n' \ 226 | f'型号: {user_dict[call.from_user.id]["size_slug"]}\n' \ 227 | f'镜像: {image_slug}\n\n' 228 | 229 | msg = bot.edit_message_text( 230 | text=f'{_t}' 231 | '请回复实例名称:\n\n' 232 | '/back 上一步', 233 | chat_id=call.from_user.id, 234 | message_id=call.message.message_id, 235 | parse_mode='HTML' 236 | ) 237 | bot.register_next_step_handler(msg, ask_create) 238 | 239 | 240 | def ask_create(m: Message): 241 | if m.text == '/back': 242 | select_image(m, data={'size': [user_dict[m.from_user.id]["size_slug"]]}) 243 | return 244 | 245 | _t = t + f'账号: {user_dict[m.from_user.id]["account"]["email"]}\n' \ 246 | f'地区: {user_dict[m.from_user.id]["region_slug"]}\n' \ 247 | f'型号: {user_dict[m.from_user.id]["size_slug"]}\n' \ 248 | f'镜像: {user_dict[m.from_user.id]["image_slug"]}\n' \ 249 | f'名称: {m.text}\n\n' 250 | markup = InlineKeyboardMarkup(row_width=2) 251 | markup.add( 252 | InlineKeyboardButton( 253 | text='上一步', 254 | callback_data=f'create_droplet?nf=get_name&image={user_dict[m.from_user.id]["image_slug"]}' 255 | ), 256 | InlineKeyboardButton( 257 | text='取消', 258 | callback_data='create_droplet?nf=cancel_create' 259 | ), 260 | ) 261 | markup.row( 262 | InlineKeyboardButton( 263 | text='创建', 264 | callback_data=f'create_droplet?nf=confirm_create&name={m.text}' 265 | ) 266 | ) 267 | 268 | bot.send_message( 269 | text=_t, 270 | chat_id=m.from_user.id, 271 | reply_markup=markup, 272 | parse_mode='HTML' 273 | ) 274 | 275 | 276 | def cancel_create(call: CallbackQuery): 277 | bot.edit_message_text( 278 | text=f'{call.message.html_text}\n\n' 279 | '已取消创建', 280 | chat_id=call.from_user.id, 281 | message_id=call.message.message_id, 282 | parse_mode='HTML' 283 | ) 284 | 285 | 286 | def confirm_create(call: CallbackQuery, data: dict): 287 | droplet_name = data['name'][0] 288 | password = password_generator() 289 | 290 | bot.edit_message_text( 291 | text=f'{call.message.html_text}\n\n' 292 | '创建实例中...', 293 | chat_id=call.from_user.id, 294 | message_id=call.message.message_id, 295 | parse_mode='HTML' 296 | ) 297 | 298 | droplet = digitalocean.Droplet( 299 | token=user_dict[call.from_user.id]['account']['token'], 300 | name=droplet_name, 301 | region=user_dict[call.from_user.id]['region_slug'], 302 | image=user_dict[call.from_user.id]['image_slug'], 303 | size_slug=user_dict[call.from_user.id]['size_slug'], 304 | user_data=set_root_password_script(password) 305 | ) 306 | droplet.create() 307 | 308 | droplet_actions = droplet.get_actions() 309 | for action in droplet_actions: 310 | while action.status != 'completed': 311 | sleep(3) 312 | action.load() 313 | droplet.load() 314 | 315 | markup = InlineKeyboardMarkup() 316 | markup.row( 317 | InlineKeyboardButton( 318 | text='查看详情', 319 | callback_data=f'droplet_detail?' 320 | f'doc_id={user_dict[call.from_user.id]["account"].doc_id}&' 321 | f'droplet_id={droplet.id}' 322 | ) 323 | ) 324 | 325 | bot.edit_message_text( 326 | text=f'{call.message.html_text}\n' 327 | f'密码: {password}\n' 328 | f'IP: {droplet.ip_address}\n\n' 329 | '实例创建完成', 330 | chat_id=call.from_user.id, 331 | reply_markup=markup, 332 | message_id=call.message.message_id, 333 | parse_mode='HTML' 334 | ) 335 | -------------------------------------------------------------------------------- /modules/delete_account.py: -------------------------------------------------------------------------------- 1 | from telebot.types import CallbackQuery 2 | 3 | from _bot import bot 4 | from utils.db import AccountsDB 5 | 6 | 7 | def delete_account(call: CallbackQuery, data: dict): 8 | doc_id = data['doc_id'][0] 9 | 10 | AccountsDB().remove(doc_id=doc_id) 11 | 12 | bot.edit_message_text( 13 | text=f'{call.message.html_text}\n\n' 14 | f'账号已删除', 15 | chat_id=call.from_user.id, 16 | message_id=call.message.message_id, 17 | parse_mode='HTML' 18 | ) 19 | -------------------------------------------------------------------------------- /modules/droplet_actions.py: -------------------------------------------------------------------------------- 1 | from telebot.types import CallbackQuery 2 | 3 | import digitalocean 4 | 5 | from _bot import bot 6 | from utils.db import AccountsDB 7 | 8 | 9 | def droplet_actions(call: CallbackQuery, data: dict): 10 | doc_id = data['doc_id'][0] 11 | droplet_id = data['droplet_id'][0] 12 | action = data['a'][0] 13 | 14 | account = AccountsDB().get(doc_id=doc_id) 15 | droplet = digitalocean.Droplet( 16 | token=account['token'], 17 | id=droplet_id 18 | ) 19 | 20 | if action in globals(): 21 | globals()[action](call, droplet) 22 | 23 | 24 | # def change_ip(call: CallbackQuery, droplet: digitalocean.Droplet): 25 | # bot.edit_message_text( 26 | # text=f'{call.message.html_text}\n\n' 27 | # '实例更换 IP 中...', 28 | # chat_id=call.from_user.id, 29 | # message_id=call.message.message_id, 30 | # parse_mode='HTML' 31 | # ) 32 | # 33 | # droplet.load() 34 | # 35 | # bot.edit_message_text( 36 | # text=f'{call.message.html_text}\n\n' 37 | # '实例更换 IP 完成,请刷新', 38 | # chat_id=call.from_user.id, 39 | # message_id=call.message.message_id, 40 | # reply_markup=call.message.reply_markup, 41 | # parse_mode='HTML' 42 | # ) 43 | 44 | 45 | def delete(call: CallbackQuery, droplet: digitalocean.Droplet): 46 | bot.edit_message_text( 47 | text=f'{call.message.html_text}\n\n' 48 | '实例删除中...', 49 | chat_id=call.from_user.id, 50 | message_id=call.message.message_id, 51 | parse_mode='HTML' 52 | ) 53 | 54 | droplet.load() 55 | droplet.destroy() 56 | 57 | bot.edit_message_text( 58 | text=f'{call.message.html_text}\n\n' 59 | f'实例已删除', 60 | chat_id=call.from_user.id, 61 | message_id=call.message.message_id, 62 | parse_mode='HTML' 63 | ) 64 | 65 | 66 | def shutdown(call: CallbackQuery, droplet: digitalocean.Droplet): 67 | bot.edit_message_text( 68 | text=f'{call.message.html_text}\n\n' 69 | '实例关机中,请稍后刷新', 70 | chat_id=call.from_user.id, 71 | message_id=call.message.message_id, 72 | reply_markup=call.message.reply_markup, 73 | parse_mode='HTML' 74 | ) 75 | 76 | droplet.load() 77 | droplet.shutdown() 78 | 79 | 80 | def reboot(call: CallbackQuery, droplet: digitalocean.Droplet): 81 | bot.edit_message_text( 82 | text=f'{call.message.html_text}\n\n' 83 | '实例重启中,请稍后刷新', 84 | chat_id=call.from_user.id, 85 | message_id=call.message.message_id, 86 | reply_markup=call.message.reply_markup, 87 | parse_mode='HTML' 88 | ) 89 | 90 | droplet.load() 91 | droplet.reboot() 92 | 93 | 94 | def power_on(call: CallbackQuery, droplet: digitalocean.Droplet): 95 | bot.edit_message_text( 96 | text=f'{call.message.html_text}\n\n' 97 | '实例开机中,请稍后刷新', 98 | chat_id=call.from_user.id, 99 | message_id=call.message.message_id, 100 | reply_markup=call.message.reply_markup, 101 | parse_mode='HTML' 102 | ) 103 | 104 | droplet.load() 105 | droplet.reboot() 106 | -------------------------------------------------------------------------------- /modules/droplet_detail.py: -------------------------------------------------------------------------------- 1 | from telebot.types import ( 2 | CallbackQuery, 3 | InlineKeyboardMarkup, 4 | InlineKeyboardButton, 5 | ) 6 | 7 | import digitalocean 8 | 9 | from _bot import bot 10 | from utils.db import AccountsDB 11 | from utils.localizer import localize_region 12 | 13 | 14 | def droplet_detail(call: CallbackQuery, data: dict): 15 | doc_id = data['doc_id'][0] 16 | droplet_id = data['droplet_id'][0] 17 | t = '实例信息\n\n' 18 | 19 | account = AccountsDB().get(doc_id=doc_id) 20 | 21 | bot.edit_message_text( 22 | text=f'{t}' 23 | f'账号: {account["email"]}\n\n' 24 | '获取实例信息中...', 25 | chat_id=call.from_user.id, 26 | message_id=call.message.message_id, 27 | parse_mode='HTML' 28 | ) 29 | 30 | droplet = digitalocean.Droplet().get_object( 31 | api_token=account['token'], 32 | droplet_id=droplet_id 33 | ) 34 | 35 | markup = InlineKeyboardMarkup() 36 | markup.row( 37 | # InlineKeyboardButton( 38 | # text='更换 IP', 39 | # callback_data=f'droplet_actions?doc_id={doc_id}&droplet_id={droplet_id}&a=change_ip' 40 | # ), 41 | InlineKeyboardButton( 42 | text='删除', 43 | callback_data=f'droplet_actions?doc_id={doc_id}&droplet_id={droplet_id}&a=delete' 44 | ), 45 | ) 46 | power_buttons = [] 47 | if droplet.status == 'active': 48 | power_buttons.extend([ 49 | InlineKeyboardButton( 50 | text='关机', 51 | callback_data=f'droplet_actions?doc_id={doc_id}&droplet_id={droplet_id}&a=shutdown' 52 | ), 53 | InlineKeyboardButton( 54 | text='重启', 55 | callback_data=f'droplet_actions?doc_id={doc_id}&droplet_id={droplet_id}&a=reboot' 56 | ) 57 | ]) 58 | else: 59 | power_buttons.append( 60 | InlineKeyboardButton( 61 | text='开机', 62 | callback_data=f'droplet_actions?doc_id={doc_id}&droplet_id={droplet_id}&a=power_on' 63 | ) 64 | ) 65 | markup.row(*power_buttons) 66 | markup.row( 67 | InlineKeyboardButton( 68 | text='刷新', 69 | callback_data=f'droplet_detail?doc_id={account.doc_id}&droplet_id={droplet_id}' 70 | ), 71 | InlineKeyboardButton( 72 | text='返回', 73 | callback_data=f'list_droplets?doc_id={account.doc_id}' 74 | ) 75 | ) 76 | 77 | bot.edit_message_text( 78 | text=f'{t}' 79 | f'账号: {account["email"]}\n' 80 | f'名称: {droplet.name}\n' 81 | f'型号: {droplet.size_slug}\n' 82 | f'地区: {localize_region(droplet.region["slug"])}\n' 83 | f'镜像: {droplet.image["distribution"]} {droplet.image["name"]}\n' 84 | f'硬盘: {droplet.disk} GB\n' 85 | f'IP: {droplet.ip_address}\n' 86 | f'内网 IP: {droplet.private_ip_address}\n' 87 | f'状态: {droplet.status}\n' 88 | f'创建时间: {droplet.created_at.split("T")[0]}\n', 89 | chat_id=call.from_user.id, 90 | message_id=call.message.message_id, 91 | parse_mode='HTML', 92 | reply_markup=markup 93 | ) 94 | -------------------------------------------------------------------------------- /modules/list_droplets.py: -------------------------------------------------------------------------------- 1 | from telebot.types import ( 2 | CallbackQuery, 3 | InlineKeyboardMarkup, 4 | InlineKeyboardButton, 5 | ) 6 | 7 | import digitalocean 8 | 9 | from _bot import bot 10 | from utils.db import AccountsDB 11 | from utils.localizer import localize_region 12 | 13 | 14 | def list_droplets(call: CallbackQuery, data: dict): 15 | doc_id = data['doc_id'][0] 16 | t = '管理实例\n\n' 17 | 18 | account = AccountsDB().get(doc_id=doc_id) 19 | 20 | bot.edit_message_text( 21 | text=f'{t}' 22 | f'账号: {account["email"]}\n\n' 23 | '获取实例中...', 24 | chat_id=call.from_user.id, 25 | message_id=call.message.message_id, 26 | parse_mode='HTML' 27 | ) 28 | 29 | droplets = digitalocean.Manager(token=account['token']).get_all_droplets() 30 | 31 | markup = InlineKeyboardMarkup() 32 | 33 | if len(droplets) == 0: 34 | markup.add( 35 | InlineKeyboardButton( 36 | text='创建实例', 37 | callback_data=f'create_droplet?nf=select_region&doc_id={account.doc_id}' 38 | ) 39 | ) 40 | 41 | bot.edit_message_text( 42 | text=f'{t}' 43 | f'账号: {account["email"]}\n\n' 44 | '没有实例', 45 | chat_id=call.from_user.id, 46 | message_id=call.message.message_id, 47 | parse_mode='HTML', 48 | reply_markup=markup 49 | ) 50 | return 51 | 52 | for droplet in droplets: 53 | markup.row( 54 | InlineKeyboardButton( 55 | text=f'{droplet.name} ({localize_region(droplet.region["slug"])}) ({droplet.size_slug})', 56 | callback_data=f'droplet_detail?doc_id={account.doc_id}&droplet_id={droplet.id}' 57 | ) 58 | ) 59 | 60 | bot.edit_message_text( 61 | text=f'{t}' 62 | f'账号: {account["email"]}\n\n' 63 | '请选择实例', 64 | chat_id=call.from_user.id, 65 | message_id=call.message.message_id, 66 | parse_mode='HTML', 67 | reply_markup=markup 68 | ) 69 | -------------------------------------------------------------------------------- /modules/manage_accounts.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from telebot.types import ( 4 | Message, 5 | CallbackQuery, 6 | InlineKeyboardMarkup, 7 | InlineKeyboardButton, 8 | ) 9 | 10 | from _bot import bot 11 | from utils.db import AccountsDB 12 | 13 | 14 | def manage_accounts(d: Union[Message, CallbackQuery]): 15 | t = '管理账号\n\n' 16 | markup = InlineKeyboardMarkup() 17 | 18 | accounts = AccountsDB().all() 19 | 20 | if len(accounts) == 0: 21 | t += '没有账号' 22 | markup.row( 23 | InlineKeyboardButton( 24 | text='添加账号', 25 | callback_data='add_account' 26 | ) 27 | ) 28 | 29 | bot.send_message( 30 | text=t, 31 | chat_id=d.from_user.id, 32 | reply_markup=markup, 33 | parse_mode='HTML' 34 | ) 35 | return 36 | 37 | markup.row( 38 | InlineKeyboardButton( 39 | text='批量测试', 40 | callback_data='batch_test_accounts' 41 | ) 42 | ) 43 | 44 | for account in accounts: 45 | markup.row( 46 | InlineKeyboardButton( 47 | text=account.get('email', 'error'), 48 | callback_data=f'account_detail?doc_id={account.doc_id}' 49 | ) 50 | ) 51 | 52 | bot.send_message( 53 | text=t, 54 | chat_id=d.from_user.id, 55 | reply_markup=markup, 56 | parse_mode='HTML' 57 | ) 58 | -------------------------------------------------------------------------------- /modules/manage_droplets.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from telebot.types import ( 4 | Message, 5 | CallbackQuery, 6 | InlineKeyboardMarkup, 7 | InlineKeyboardButton, 8 | ) 9 | 10 | from _bot import bot 11 | from utils.db import AccountsDB 12 | 13 | 14 | def manage_droplets(d: Union[Message, CallbackQuery]): 15 | t = '管理实例\n\n' 16 | markup = InlineKeyboardMarkup() 17 | 18 | accounts = AccountsDB().all() 19 | 20 | if len(accounts) == 0: 21 | markup.row( 22 | InlineKeyboardButton( 23 | text='添加账号', 24 | callback_data='add_account' 25 | ) 26 | ) 27 | 28 | bot.send_message( 29 | text=f'{t}' 30 | f'你还没有添加账号', 31 | chat_id=d.from_user.id, 32 | reply_markup=markup, 33 | parse_mode='HTML' 34 | ) 35 | return 36 | 37 | for account in accounts: 38 | markup.add( 39 | InlineKeyboardButton( 40 | text=account['email'], 41 | callback_data=f'list_droplets?doc_id={account.doc_id}' 42 | ) 43 | ) 44 | 45 | bot.send_message( 46 | text=f'{t}' 47 | f'请选择账号', 48 | chat_id=d.from_user.id, 49 | parse_mode='HTML', 50 | reply_markup=markup 51 | ) 52 | -------------------------------------------------------------------------------- /modules/start.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | from telebot.types import ( 4 | Message, 5 | InlineKeyboardMarkup, 6 | InlineKeyboardButton, 7 | ) 8 | 9 | from _bot import bot 10 | 11 | bot_name = environ.get('bot_name', 'Digital Ocean 小助手') 12 | 13 | 14 | def start(d: Message): 15 | markup = InlineKeyboardMarkup(row_width=2) 16 | markup.add( 17 | InlineKeyboardButton( 18 | text='添加账号', 19 | callback_data='add_account' 20 | ), 21 | InlineKeyboardButton( 22 | text='管理账号', 23 | callback_data='manage_accounts' 24 | ), 25 | InlineKeyboardButton( 26 | text='创建实例', 27 | callback_data='create_droplet' 28 | ), 29 | InlineKeyboardButton( 30 | text='管理实例', 31 | callback_data='manage_droplets' 32 | ), 33 | ) 34 | t = f'欢迎使用 {bot_name}\n\n' \ 35 | '你可以管理 DigitalOcean 账号,创建实例等\n\n' \ 36 | '快捷命令:\n' \ 37 | '/aa 添加账号 /ma 管理账号 /bta 批量测试账号\n' \ 38 | '/cd 创建实例 /md 管理实例' 39 | 40 | bot.send_message( 41 | text=t, 42 | chat_id=d.from_user.id, 43 | parse_mode='HTML', 44 | reply_markup=markup 45 | ) 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realByg/digitalocean-helper-bot/3849c231a931f4f01017a612a94134a5e047b3c1/requirements.txt -------------------------------------------------------------------------------- /utils/db.py: -------------------------------------------------------------------------------- 1 | from tinydb import TinyDB, where 2 | from datetime import datetime 3 | 4 | db_file = 'db.json' 5 | 6 | 7 | class AccountsDB: 8 | 9 | def __init__(self): 10 | db = TinyDB(db_file) 11 | self.accounts = db.table('Accounts') 12 | 13 | def save(self, email: str, token: str, remarks: str = ''): 14 | email = email.strip() 15 | token = token.strip() 16 | date = datetime.today().strftime('%Y-%m-%d') 17 | 18 | if self.accounts.get(where('token') == token): 19 | raise Exception('Token Exists') 20 | else: 21 | self.accounts.insert({ 22 | 'email': email, 23 | 'token': token, 24 | 'remarks': remarks, 25 | 'date': date 26 | }) 27 | 28 | def all(self): 29 | return self.accounts.all() 30 | 31 | def get(self, doc_id: int): 32 | return self.accounts.get(doc_id=int(doc_id)) 33 | 34 | def remove(self, doc_id: int): 35 | self.accounts.remove(doc_ids=[int(doc_id)]) 36 | -------------------------------------------------------------------------------- /utils/localizer.py: -------------------------------------------------------------------------------- 1 | def localize_region(slug: str): 2 | regions = [ 3 | {'slug': 'nyc1', 'name': '纽约 1'}, {'slug': 'nyc2', 'name': '纽约 2'}, {'slug': 'nyc3', 'name': '纽约 3'}, 4 | {'slug': 'sfo1', 'name': '旧金山 1'}, {'slug': 'sfo2', 'name': '旧金山 2'}, {'slug': 'sfo3', 'name': '旧金山 3'}, 5 | {'slug': 'ams2', 'name': '阿姆斯特丹 2'}, {'slug': 'ams3', 'name': '阿姆斯特丹 3'}, 6 | {'slug': 'sgp1', 'name': '新加坡 1'}, {'slug': 'lon1', 'name': '伦敦 1'}, 7 | {'slug': 'fra1', 'name': '法国 1'}, {'slug': 'blr1', 'name': '班加罗尔 1'}, 8 | {'slug': 'tor1', 'name': '多伦多 1'}, 9 | ] 10 | 11 | for region in regions: 12 | if region['slug'] == slug: 13 | return region['name'] 14 | 15 | return slug 16 | -------------------------------------------------------------------------------- /utils/password_generator.py: -------------------------------------------------------------------------------- 1 | from random import choice 2 | 3 | 4 | def password_generator(): 5 | a = 'QWERTYUIOPASDFGHJKLZXCVBNM' 6 | b = 'qwertyuiopasdfghjklzxcvbnm' 7 | c = '1234567890' 8 | 9 | p = '' 10 | for i in range(3): 11 | p += choice(a) 12 | p += choice(b) 13 | p += choice(c) 14 | 15 | return p 16 | -------------------------------------------------------------------------------- /utils/set_root_password_script.py: -------------------------------------------------------------------------------- 1 | def set_root_password_script(password: str): 2 | return '#!/bin/bash\n' \ 3 | f'echo root:{password} | sudo chpasswd root\n' \ 4 | 'sudo sed -i "s/^.*PermitRootLogin.*/PermitRootLogin yes/g" /etc/ssh/sshd_config\n' \ 5 | 'sudo sed -i "s/^.*PasswordAuthentication.*/PasswordAuthentication yes/g" /etc/ssh/sshd_config\n' \ 6 | 'sudo systemctl restart sshd\n' 7 | --------------------------------------------------------------------------------