Discord Bot経由でレシートをGoogle Drive/Sheetに保存したい
作りたいもの
Discord Botに次のような機能を搭載したものが欲しい
- レシートの値段をspread sheet等に保存
- 買ったもののカテゴリーも上と同じように保存
- 画像をGoogle Driveに保存
作ろうと思ったわけ
プログラミングを始めて2年になりますが、個人開発や業務でのプログラミング経験があまりないため、何か自分で作ってみたいと思いました。普段は競技プログラミングに取り組んでいますが、実用的なツールにも挑戦してみたいと考えています。
私情ですが、いつもGoogle Driveにレシートの画像をアップロードし、月末にまとめてGoogle Spreadsheetに記録して(もらって)いるのですが、量が多くなりすぎて大変です。
そこで、日常的に使っているDiscord上で、簡単にレシートの操作できるBotを作ってみようと思いました。Discordは私の普段の連絡手段でもあり、Botの開発経験も少しあるため、選びました。
実装
今回はDiscord.pyを使っていきます。
メインの機能として
- ユーザーがレシートの画像を送信
- Botがそれに返信し、ユーザーがTextInputに値段及びカテゴリーを入力する
- BotがDriveに画像を保存、スプレッドシートに入力された内容を記録
必要な機能に
- レシートを送信するチャネルの選択
- 編集するSpreadsheetのURLの設定
- Upload先のURLの設定
Config Class
Config Classを作り、アプリの各種設定を一括管理していきます。
主な機能として
-
設定ファイル(.env/.json)の読み込み
読み取り専用の.envファイルにBotのTokenや利用できるユーザーのIDを保存します。
利用できるユーザーのIDを定めておくことにより、他の人がBotを通して各種Googleアプリにアクセスするのを防いでみます。
.jsonはBotを操作時に変更可能なURLやIDを保存するためにしようします。 -
Google Workspace APIの認証
DriveやSpreadsheetにアクセスできるようにします。
こちらを参考にしながら作りました。
https://developers.google.com/workspace/sheets/api/quickstart/python?hl=ja -
各種設定を.jsonに保存
class Config:
def __init__(self, config_file='config.json', token_file='token.json'):
self.config_file = config_file
self.token_file = token_file
load_dotenv()
self.allowed_user_id = int(os.getenv('allowed_user_id', '0'))
self.bot_token = os.getenv('BOT_TOKEN')
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
data = json.load(f)
except Exception as e:
print(f"Error loading config file: {e}")
exit(1)
self.google_drive_folder_id = data.get('google_drive_folder_id', '')
self.spreadsheet_id = data.get('spreadsheet_id', '')
self.reciept_channel_id = int(data.get('reciept_channel_id', 0))
cats = data.get('categories', [])
self.categories = cats if isinstance(cats, list) else [c.strip() for c in cats.split(',') if c.strip()]
scopes = [
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive.file',
]
try:
self.creds = service_account.Credentials.from_service_account_file(
self.token_file, scopes=scopes
)
self.creds.refresh(Request())
except Exception as e:
print(f"Google API credentials error: {e}")
exit(1)
def _save(self, key, value):
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
data = json.load(f)
data[key] = value
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
except Exception as e:
print(f"Error updating config file: {e}")
def set_folder_id(self, folder_id):
self.google_drive_folder_id = folder_id
self._save('google_drive_folder_id', folder_id)
def set_spreadsheet_id(self, spreadsheet_id):
self.spreadsheet_id = spreadsheet_id
self._save('spreadsheet_id', spreadsheet_id)
def set_receipt_channel_id(self, channel_id):
self.reciept_channel_id = channel_id
self._save('reciept_channel_id', channel_id)
def add_category(self, category):
if category not in self.categories:
self.categories.append(category)
self._save('categories', self.categories)
set_id
系を助長に書いてしまったのは反省です。あまり良い実装が思いつきませんでした。
DriveとSheetにUploadするクラス
こちらも認証同様、この2つを参考に書きました。
# ----- Google Drive Uploader -----
class GoogleDriveUploader:
def __init__(self, folder_id, config):
self.folder_id = folder_id
self.config = config
def upload_file(self, file_path):
try:
drive = build('drive', 'v3', credentials=self.config.creds)
meta = {'name': os.path.basename(file_path), 'parents': [self.folder_id]}
media = MediaFileUpload(file_path, mimetype='image/jpeg')
res = drive.files().create(body=meta, media_body=media, fields='id').execute()
return f"https://drive.google.com/uc?id={res.get('id')}"
except Exception as e:
return f"[Error uploading file]: {e}"
# ----- Spreadsheet Recorder -----
class SpreadSheetRecorder:
def __init__(self, spreadsheet_id, config):
self.spreadsheet_id = spreadsheet_id
self.config = config
def record_data(self, date, image_url, category, price):
try:
gc = gspread.service_account(filename=self.config.token_file)
sheet = gc.open_by_key(self.spreadsheet_id).sheet1
sheet.append_row([date, image_url, category, price])
return "saved"
except Exception as e:
return f"[Error recording data]: {e}"
ドキュメント通りに書いたらちゃんと動きました。よかったです。
Discord Bot側の実装
Discord Bot側の実装は以前の作品から引っ張ってきたので割愛します。
このような感じになりました。
写真を送るとBotが反応して、カテゴリーをユーザーが選択
ボタンを押すとフォームが出てくるので金額を入力する
アップロードに成功すると、このようなメッセージが返ってくる
(黒塗り部分はリンク)
今はこのような感じに保存しています。
アップロードもちゃんとできてますね。
良さそうです。
感想
金額入力も少しばかり手間なので、精度はあまり良くなくて良いので、画像認識等で値段を抽出するようにしたいですね。また今度時間ができたらやってみます。
Discussion