├── .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 | 
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 |
--------------------------------------------------------------------------------