diff --git a/.gitignore b/.gitignore index 0a1cc7e..7d95833 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ share/python-wheels/ MANIFEST .DS_Store config.json +credentials.json # PyInstaller # Usually these files are written by a python script from a template diff --git a/README.md b/README.md index 2bf4848..2ccb30e 100644 --- a/README.md +++ b/README.md @@ -18,33 +18,13 @@ Google 스프레드시트를 기반으로 지정된 문구들을 자동적으로 ## Options config.example.josn 파일을 복사 후 config.json으로 이름 변경 후 작성해주세요. -```json -{ - "token": "여기에는 토큰을 넣어주세요", - // 봇을 실행할 계정에서 "설정 -> 기타 설정의 API -> 액세스 토큰 생성 -> `노트를 작성하거나 삭제합니다` 체크 후 나온 값" - "origin": "여기에는 서버 주소를 넣어주세요", - // (k.lapy.link, phater.live... etc) - "max_duplicate": 3, - // 중복으로 처리할 대사의 최대 수 - "rate": 60, - // 자동 노트 게시 간격 (분 단위) - "visibility": "home", - // 공개 범위 (public, home, followers, specified) - "worksheet": "여기에는 구글 스프레드시트 주소를 넣어주세요", - "template": { - "auto": "{text}", - // 자동으로 게시되는 노트의 템플릿 {text} = 내용 {from} = 예시 시트 기준 "대사 위치" {number} = 예시 시트 기준 "대사 번호" (꼭 숫자일 필요 없음) - "mention": "{text}\n \n{from}에서 발췌됨. ({number}번 대사)" - // 답장으로 게시되는 노트의 템플릿 {text} = 내용 {from} = 예시 시트 기준 "대사 위치" {number} = 예시 시트 기준 "대사 번호" (꼭 숫자일 필요 없음) - } -} -``` ## How ### Requirements * Git * Google Cloud Service Account * 구글 스프레드시트 연동을 위해 작업이 필요합니다. ([gspread 문서 참조](https://docs.gspread.org/en/latest/oauth2.html)) + * 생성한 Service Account 인증 json 키 파일의 위치를 config.json에 작성해주세요. * Ubuntu 20.04+ or Windows 10+ * Python 3.10+ * Google 스프레드시트 ([예시 스프레드시트](https://docs.google.com/spreadsheets/d/1nO70lwFFkyyK8AtVE4fWO7lW7KDtM5pNudGJydTaQdk/edit)) diff --git a/config.example.json b/config.example.json index 6a7ae71..6df0e75 100644 --- a/config.example.json +++ b/config.example.json @@ -1,6 +1,7 @@ { "token": "YOUR_ACCESS_TOKEN_HERE", "origin": "YOUR_MISSKEY_SERVER_HERE", + "credentialsJSONFile": "./credentials.json", "duplicateQueueAfter": 3, "rate": 60, "startFrom": 30, diff --git a/exts/post.py b/exts/post.py index 85e8394..6a6f526 100644 --- a/exts/post.py +++ b/exts/post.py @@ -14,13 +14,13 @@ class Post(commands.Cog): @tasks.loop(seconds=1800) async def _postLine(self) -> None: - line = self.bot.get_random_line() + line = await self.bot.get_random_line() while line.text in self.posted: - line = self.bot.get_random_line() + line = await self.bot.get_random_line() template = self.bot.config.note result = template.replace("{text}", line.text).replace("{from}", line.where).replace("{number}", line.number) await self.bot.client.note.action.send(content=result, visibility=self.visibility) - self.posted.append(line) + self.posted.append(line.text) if len(self.posted) > self.max_count: self.posted.pop(0) @@ -37,3 +37,5 @@ async def setup(bot: Bot): await asyncio.sleep(1) now = datetime.now() await cog._postLine.start() + else: + await cog._postLine.start() diff --git a/main.py b/main.py index 798a98b..903cf27 100644 --- a/main.py +++ b/main.py @@ -2,9 +2,10 @@ import asyncio import json import random -import gspread +import gspread_asyncio from aiohttp import ClientWebSocketResponse -from gspread.worksheet import Worksheet +from google.oauth2.service_account import Credentials +from gspread_asyncio import AsyncioGspreadWorksheet as Worksheet from mipac.models.notification import NotificationNote from mipa.ext import commands @@ -15,6 +16,7 @@ class Config: raw = json.load(file) self.token = raw.get("token") self.origin = raw.get("origin") + self.credentials = raw.get("credentialsJSONFile") self.max = raw.get("duplicateQueueAfter") self.rate = raw.get("rate") self.start_time = raw.get("startFrom") @@ -26,53 +28,72 @@ class Config: if any([ self.token is None, self.origin is None, + self.credentials is None, self.worksheet is None, self.note is None, self.reply is None ]): raise ValueError("config.json 파일에 일부 필수 값이 누락되었습니다.") + def get_creds(self): + creds = Credentials.from_service_account_file(self.credentials) + scoped = creds.with_scopes([ + "https://spreadsheets.google.com/feeds", + "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/drive", + ]) + return scoped + class Line: - def __init__(self, row: int, bot: "Autoposter") -> None: - sheet = bot.get_worksheet() - self.location = row - res = sheet.get(f"D{row}") - self.text = res[0][0].strip() - res = sheet.get(f"C{row}") - self.where = res[0][0].strip() - res = sheet.get(f"B{row}") - self.number = res[0][0].strip() + def __init__(self, data: dict) -> None: + self.location = data["row"] + 2 + self.text = data["text"] + self.where = data["where"] + self.number = data["number"] + + @classmethod + async def from_number(cls: "Line", row: int, sheet: Worksheet) -> "Line": + res = await sheet.get(f"D{row}") + text = res[0][0].strip() + res = await sheet.get(f"C{row}") + where = res[0][0].strip() + res = await sheet.get(f"B{row}") + number = res[0][0].strip() + data = {"row": row, "number": number, "where": where, "text": text} + return cls(data) class Autoposter(commands.Bot): def __init__(self): super().__init__() self.config: Config = Config("./config.json") + self.agcm = gspread_asyncio.AsyncioGspreadClientManager(self.config.get_creds) - def get_worksheet(self) -> Worksheet: - gc = gspread.service_account() - sh = gc.open_by_url(self.config.worksheet) - worksheet = sh.get_worksheet(0) + async def get_worksheet(self) -> Worksheet: + client = await self.agcm.authorize() + spreadsheet = await client.open_by_url(self.config.worksheet) + worksheet = await spreadsheet.get_worksheet(0) return worksheet - def get_random_line(self) -> str: - sheet: Worksheet = self.get_worksheet() - response = sheet.get("F4") + async def get_random_line(self) -> Line: + sheet: Worksheet = await self.get_worksheet() + response = await sheet.get("F4") if response is None or response == "": return count = int(response[0][0]) result = random.randint(1, count) number = result + 2 - return Line(number, self) + return await Line.from_number(number, sheet) - def get_line(self, number: int) -> str: - return Line(number, self) + async def get_line(self, number: int) -> Line: + sheet: Worksheet = await self.get_worksheet() + return await Line.from_number(number, sheet) - async def _connect_channel(self): + async def _connect_channel(self) -> None: await self.router.connect_channel(['main', 'global']) - async def on_ready(self, ws: ClientWebSocketResponse): + async def on_ready(self, ws: ClientWebSocketResponse) -> None: print(f"Connected as @{self.user.username}@{self.config.origin}") await self._connect_channel() extensions = [ @@ -81,11 +102,11 @@ class Autoposter(commands.Bot): for extension in extensions: await self.load_extension(extension) - async def on_reconnect(self, ws: ClientWebSocketResponse): + async def on_reconnect(self, ws: ClientWebSocketResponse) -> None: print("Disconnected from server. Reconnecting...") await self._connect_channel() - async def on_mention(self, notice: NotificationNote): + async def on_mention(self, notice: NotificationNote) -> None: if notice.note.reply_id is not None: return diff --git a/requirements.txt b/requirements.txt index f781c77..c968f3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -gspread +gspread-asyncio mipa python-dotenv \ No newline at end of file