Taka005 commited on
Commit
f8c7305
·
1 Parent(s): a28bbcf
Files changed (11) hide show
  1. base-gd-2.png +0 -0
  2. base-gd-3.png +0 -0
  3. base-gd-4.png +0 -0
  4. base-gd-5.png +0 -0
  5. base-gd.png +0 -0
  6. base-w.png → base.png +0 -0
  7. bot.py +0 -444
  8. icon.png +0 -0
  9. main.py +112 -0
  10. modules/emojistore.py +0 -143
  11. quote.png +0 -0
base-gd-2.png DELETED
Binary file (301 kB)
 
base-gd-3.png DELETED
Binary file (179 kB)
 
base-gd-4.png DELETED
Binary file (35 kB)
 
base-gd-5.png DELETED
Binary file (34.9 kB)
 
base-gd.png CHANGED
base-w.png → base.png RENAMED
File without changes
bot.py DELETED
@@ -1,444 +0,0 @@
1
- import logging
2
- from misskey import Misskey, NoteVisibility
3
- import websockets
4
- import asyncio, aiohttp
5
- import json
6
- import datetime
7
- import sys
8
- import traceback
9
- import re
10
- import math
11
- import time
12
- import textwrap
13
- import requests
14
-
15
- try:
16
- import config_my as config
17
- except ImportError:
18
- import config
19
-
20
- from PIL import Image, ImageDraw, ImageFont, ImageEnhance
21
- from pilmoji import Pilmoji
22
- from io import BytesIO
23
- from modules.emojistore import EmojiStore
24
- import sqlite3
25
-
26
- logging.getLogger("websockets").setLevel(logging.INFO)
27
- logging.getLogger("PIL.Image").setLevel(logging.ERROR)
28
- logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR)
29
-
30
- WS_URL = f'wss://{config.MISSKEY_INSTANCE}/streaming?i={config.MISSKEY_TOKEN}'
31
-
32
- MISSKEY_EMOJI_REGEX = re.compile(r':([a-zA-Z0-9_]+)(?:@?)(|[a-zA-Z0-9\.-]+):')
33
-
34
- _tmp_cli = Misskey(config.MISSKEY_INSTANCE, i=config.MISSKEY_TOKEN)
35
- i = _tmp_cli.i()
36
-
37
- eStore = EmojiStore(sqlite3.connect('emoji_cache.db'))
38
-
39
- session = requests.Session()
40
- session.headers.update({
41
- 'User-Agent': f'Mozilla/5.0 (Linux; x64; Misskey Bot; {i["id"]}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
42
- })
43
-
44
- msk = Misskey(config.MISSKEY_INSTANCE, i=config.MISSKEY_TOKEN, session=session)
45
-
46
- MY_ID = i['id']
47
- ACCT = f'@{i["username"]}'
48
- print('Bot user id: ' + MY_ID)
49
-
50
- BASE_GRADATION_IMAGE = Image.open('base-gd-5.png')
51
- BASE_WHITE_IMAGE = Image.open('base-w.png')
52
-
53
- FONT_FILE = 'fonts/MPLUSRounded1c-Regular.ttf'
54
- FONT_FILE_SERIF = 'fonts/NotoSerifJP-Regular.otf'
55
- FONT_FILE_OLD_JAPANESE = 'fonts/YujiSyuku-Regular.ttf'
56
- FONT_FILE_POP = 'fonts/MochiyPopPOne-Regular.ttf'
57
-
58
- #MPLUS_FONT_TEXT = ImageFont.truetype(FONT_FILE, size=45)
59
- #MPLUS_FONT_NAME = ImageFont.truetype(FONT_FILE, size=30)
60
- MPLUS_FONT_16 = ImageFont.truetype('fonts/MPLUSRounded1c-Regular.ttf', size=16)
61
-
62
- session = aiohttp.ClientSession()
63
-
64
- default_format = '%(asctime)s:%(name)s: %(levelname)s:%(message)s'
65
-
66
- logging.basicConfig(level=logging.DEBUG, filename='debug.log', encoding='utf-8', format=default_format)
67
- # also write log to stdout
68
- stdout_handler = logging.StreamHandler(sys.stdout)
69
- stdout_handler.setLevel(logging.DEBUG)
70
- stdout_handler.setFormatter(logging.Formatter(default_format))
71
- logging.getLogger().addHandler(stdout_handler)
72
-
73
- logger = logging.getLogger('miq-fedi')
74
- logger.info('Starting')
75
- def parse_misskey_emoji(host, tx):
76
- emojis = []
77
- for emoji in MISSKEY_EMOJI_REGEX.findall(tx):
78
- h = emoji[1] or host
79
- if h == '.':
80
- h = host
81
- e = eStore.get(h, emoji[0])
82
- if e:
83
- emojis.append(e)
84
- return emojis
85
-
86
- def remove_mentions(text, mymention):
87
- mentions = sorted(re.findall(r'(@[a-zA-Z0-9_@\.]+)', text), key=lambda x: len(x), reverse=True)
88
-
89
- for m in mentions:
90
- if m == mymention:
91
- continue
92
- else:
93
- text = text.replace(m, '')
94
-
95
- return text
96
-
97
- def draw_text(im, ofs, string, font='fonts/MPLUSRounded1c-Regular.ttf', size=16, color=(0,0,0,255), split_len=None, padding=4, auto_expand=False, emojis: list = [], disable_dot_wrap=False):
98
-
99
- draw = ImageDraw.Draw(im)
100
- fontObj = ImageFont.truetype(font, size=size)
101
-
102
- # 改行、句読点(。、.,)で分割した後にさらにワードラップを行う
103
- pure_lines = []
104
- pos = 0
105
- l = ''
106
-
107
- if not disable_dot_wrap:
108
- for char in string:
109
- if char == '\n':
110
- pure_lines.append(l)
111
- l = ''
112
- pos += 1
113
- elif char == '、' or char == ',':
114
- pure_lines.append(l + ('、' if char == '、' else ','))
115
- l = ''
116
- pos += 1
117
- elif char == '。' or char == '.':
118
- pure_lines.append(l + ('。' if char == '。' else '.'))
119
- l = ''
120
- pos += 1
121
- else:
122
- l += char
123
- pos += 1
124
-
125
- if l:
126
- pure_lines.append(l)
127
- else:
128
- pure_lines = string.split('\n')
129
-
130
- lines = []
131
-
132
- for line in pure_lines:
133
- lines.extend(textwrap.wrap(line, width=split_len))
134
-
135
- dy = 0
136
-
137
- draw_lines = []
138
-
139
-
140
- # 計算
141
- for line in lines:
142
- tsize = fontObj.getsize(line)
143
-
144
- ofs_y = ofs[1] + dy
145
- t_height = tsize[1]
146
-
147
- x = int(ofs[0] - (tsize[0]/2))
148
- #draw.text((x, ofs_y), t, font=fontObj, fill=color)
149
- draw_lines.append((x, ofs_y, line))
150
- ofs_y += t_height + padding
151
- dy += t_height + padding
152
-
153
- # 描画
154
- adj_y = -30 * (len(draw_lines)-1)
155
- for dl in draw_lines:
156
- with Pilmoji(im) as p:
157
- p.text((dl[0], (adj_y + dl[1])), dl[2], font=fontObj, fill=color, emojis=emojis, emoji_position_offset=(-4, 4))
158
-
159
- real_y = ofs[1] + adj_y + dy
160
-
161
- return (0, dy, real_y)
162
-
163
-
164
- receivedNotes = set()
165
-
166
- async def on_post_note(note):
167
- pass
168
-
169
- async def on_mention(note):
170
- # HTLとGTLを監視している都合上重複する恐れがあるため
171
- if note['id'] in receivedNotes:
172
- return
173
-
174
- receivedNotes.add(note['id'])
175
-
176
- command = False
177
-
178
- childLogger = logger.getChild(note["id"])
179
-
180
- forceRun = '/make' in note['text']
181
- if forceRun:
182
- childLogger.info('forceRun enabled')
183
-
184
- # 他のメンション取り除く
185
- split_text = note['text'].split(' ')
186
- new_st = []
187
-
188
- note['text'] = remove_mentions(note['text'], ACCT)
189
-
190
- if (note['text'].strip() == '') and (not forceRun):
191
- childLogger.info('text is empty, ignoring')
192
- return
193
-
194
- try:
195
- content = note['text'].strip().split(' ', 1)[1].strip()
196
- command = True
197
- except IndexError:
198
- logger.getChild(f'{note["id"]}').info('no command found, ignoring')
199
- pass
200
-
201
- # メンションだけされた?
202
- if note.get('reply'):
203
-
204
- reply_note = note['reply']
205
-
206
- # ボットの投稿への返信の場合は応答しない
207
- if reply_note['user']['id'] == MY_ID:
208
- childLogger.info('this is reply to myself, ignoring')
209
- return
210
-
211
- reply_note['text'] = remove_mentions(reply_note['text'], None)
212
-
213
- if not reply_note['text'].strip():
214
- childLogger.info('reply text is empty, ignoring')
215
- return
216
-
217
- if reply_note['cw']:
218
- reply_note['text'] = reply_note['cw'] + '\n' + reply_note['text']
219
-
220
- username = note["user"]["name"] or note["user"]["username"]
221
-
222
- target_user = msk.users_show(reply_note['user']['id'])
223
-
224
- if '#noquote' in target_user.get('description', ''):
225
- childLogger.info(f'{reply_note["user"]["id"]} does not allow quoting, rejecting')
226
- msk.notes_create(text='このユーザーは引用を許可していません\nThis user does not allow quoting.', reply_id=note['id'])
227
- return
228
-
229
- if not (reply_note['visibility'] in ['public', 'home']):
230
- childLogger.info('visibility is not public, rejecting')
231
- msk.notes_create(text='この投稿はプライベートであるため、処理できません。\nThis post is private and cannot be processed.', reply_id=note['id'])
232
- return
233
-
234
- # 引用する
235
- img = BASE_WHITE_IMAGE.copy()
236
- # アイコン画像ダウンロード
237
- if not reply_note['user'].get('avatarUrl'):
238
- childLogger.info('user has no avatar, rejecting')
239
- msk.notes_create(text='アイコン画像がないので作れません\nWe can\'t continue because user has no avatar.', reply_id=note['id'])
240
- return
241
-
242
- childLogger.info('downloading avatar image( ' + reply_note['user']['avatarUrl'] + ' )')
243
-
244
- async with session.get(reply_note['user']['avatarUrl']) as resp:
245
- if resp.status != 200:
246
- msk.notes_create(text='アイコン画像ダウンロードに失敗しました\nFailed to download avatar image.', reply_id=note['id'])
247
- return
248
- avatar = await resp.read()
249
-
250
-
251
- childLogger.info('avatar image downloaded')
252
- childLogger.info('generating image')
253
-
254
- icon = Image.open(BytesIO(avatar))
255
- icon = icon.resize((720, 720), Image.ANTIALIAS)
256
- icon = icon.convert('L') # グレースケール変換
257
- icon_filtered = ImageEnhance.Brightness(icon)
258
-
259
- img.paste(icon_filtered.enhance(0.7), (0,0))
260
-
261
- # 黒グラデ合成
262
- img.paste(BASE_GRADATION_IMAGE, (0,0), BASE_GRADATION_IMAGE)
263
-
264
- # テキスト合成
265
- tx = ImageDraw.Draw(img)
266
-
267
- base_x = 890
268
-
269
- font_path = FONT_FILE
270
-
271
- if '%serif' in note['text']:
272
- font_path = FONT_FILE_SERIF
273
- elif '%pop' in note['text']:
274
- font_path = FONT_FILE_POP
275
- elif '%oldjp' in note['text']:
276
- font_path = FONT_FILE_OLD_JAPANESE
277
-
278
- # 文章描画
279
- emojis = parse_misskey_emoji(config.MISSKEY_INSTANCE, reply_note['text'])
280
- tsize_t = draw_text(img, (base_x, 270), note['reply']['text'], font=font_path, size=45, color=(255,255,255,255), split_len=16, auto_expand=True, emojis=emojis)
281
-
282
- # 名前描画
283
- uname = reply_note['user']['name'] or reply_note['user']['username']
284
- name_y = tsize_t[2] + 40
285
- user_emojis = parse_misskey_emoji(config.MISSKEY_INSTANCE, uname)
286
- tsize_name = draw_text(img, (base_x, name_y), uname, font=font_path, size=25, color=(255,255,255,255), split_len=25, emojis=user_emojis, disable_dot_wrap=True)
287
-
288
- # ID描画
289
- id = reply_note['user']['username']
290
- id_y = name_y + tsize_name[1] + 4
291
- tsize_id = draw_text(img, (base_x, id_y), f'(@{id}@{reply_note["user"]["host"] or config.MISSKEY_INSTANCE})', font=font_path, size=18, color=(180,180,180,255), split_len=45, disable_dot_wrap=True)
292
-
293
- # クレジット
294
- tx.text((980, 694), '<Make it a quote for Fedi> by CyberRex', font=MPLUS_FONT_16, fill=(120,120,120,255))
295
-
296
- childLogger.info('image generated')
297
-
298
-
299
- # ドライブにアップロード
300
- childLogger.info('uploading image')
301
- try:
302
- data = BytesIO()
303
- img.save(data, format='JPEG')
304
- data.seek(0)
305
- for i in range(5):
306
- try:
307
- f = msk.drive_files_create(file=data, name=f'{datetime.datetime.utcnow().timestamp()}.jpg')
308
- msk.drive_files_update(file_id=f['id'], comment=f'"{reply_note["text"][:400]}" —{reply_note["user"]["name"]}')
309
- except:
310
- childLogger.info('upload failed, retrying (attempt ' + str(i) + ')')
311
- continue
312
- break
313
- else:
314
- childLogger.error('upload failed')
315
- raise Exception('Image upload failed.')
316
- except Exception as e:
317
- childLogger.error('upload failed')
318
- childLogger.error(traceback.format_exc())
319
- if 'INTERNAL_ERROR' in str(e):
320
- msk.notes_create('Internal Error occured in Misskey!', reply_id=note['id'])
321
- return
322
- if 'RATE_LIMIT_EXCEEDED' in str(e):
323
- msk.notes_create('利用殺到による一時的なAPI制限が発生しました。しばらく時間を置いてから再度お試しください。\nA temporary API restriction has occurred due to overwhelming usage. Please wait for a while and try again.', reply_id=note['id'])
324
- return
325
- if 'YOU_HAVE_BEEN_BLOCKED' in str(e):
326
- msk.notes_create(f'@{note["user"]["username"]}@{note["user"]["host"] or config.MISSKEY_INSTANCE}\n引用元のユーザーからブロックされています。\nI am blocked by the user who posted the original post.', reply_id=note['id'])
327
- return
328
- msk.notes_create('画像アップロードに失敗しました\nFailed to upload image.\n```plaintext\n' + traceback.format_exc() + '\n```', reply_id=note['id'])
329
- return
330
-
331
- childLogger.info('image uploaded')
332
- childLogger.info('posting')
333
-
334
- try:
335
- msk.notes_create(text='.', file_ids=[f['id']], reply_id=note['id'])
336
- except Exception as e:
337
- childLogger.error('post failed')
338
- childLogger.error(traceback.format_exc())
339
- return
340
-
341
- childLogger.info('Finshed')
342
-
343
- return
344
-
345
-
346
- if command:
347
-
348
- if content == 'ping':
349
-
350
- postdate = datetime.datetime.fromisoformat(note['createdAt'][:-1]).timestamp()
351
- nowdate = datetime.datetime.utcnow().timestamp()
352
- sa = nowdate - postdate
353
- text = f'{sa*1000:.2f}ms'
354
- msk.notes_create(text=text, reply_id=note['id'])
355
-
356
-
357
-
358
- async def on_followed(user):
359
- try:
360
- msk.following_create(user['id'])
361
- except:
362
- pass
363
-
364
- async def main():
365
-
366
- logger.info(f'Connecting to {config.MISSKEY_INSTANCE}...')
367
- async with websockets.connect(WS_URL) as ws:
368
- reconnect_counter = 0
369
- logger.info(f'Connected to {config.MISSKEY_INSTANCE}')
370
- logger.info('Attemping to watching timeline...')
371
- p = {
372
- 'type': 'connect',
373
- 'body': {
374
- 'channel': 'globalTimeline',
375
- 'id': 'GTL1'
376
- }
377
- }
378
- await ws.send(json.dumps(p))
379
- p = {
380
- 'type': 'connect',
381
- 'body': {
382
- 'channel': 'homeTimeline',
383
- 'id': 'HTL1'
384
- }
385
- }
386
- await ws.send(json.dumps(p))
387
- p = {
388
- 'type': 'connect',
389
- 'body': {
390
- 'channel': 'main'
391
- }
392
- }
393
- await ws.send(json.dumps(p))
394
-
395
- logger.info('Now watching timeline...')
396
- while True:
397
- data = await ws.recv()
398
- j = json.loads(data)
399
- # print(j)
400
-
401
- if j['type'] == 'channel':
402
-
403
- if j['body']['type'] == 'note':
404
- note = j['body']['body']
405
- try:
406
- await on_post_note(note)
407
- except Exception as e:
408
- print(traceback.format_exc())
409
- logger.error(traceback.format_exc())
410
- continue
411
-
412
- if j['body']['type'] == 'mention':
413
- note = j['body']['body']
414
- try:
415
- await on_mention(note)
416
- except Exception as e:
417
- print(traceback.format_exc())
418
- logger.error(traceback.format_exc())
419
- continue
420
-
421
- if j['body']['type'] == 'followed':
422
- try:
423
- await on_followed(j['body']['body'])
424
- except Exception as e:
425
- print(traceback.format_exc())
426
- logger.error(traceback.format_exc())
427
- continue
428
-
429
-
430
- reconnect_counter = 0
431
-
432
- while True:
433
- try:
434
- asyncio.get_event_loop().run_until_complete(main())
435
- except KeyboardInterrupt:
436
- break
437
- except:
438
- time.sleep(10)
439
- reconnect_counter += 1
440
- logger.warning('Disconnected from WebSocket. Reconnecting...')
441
- if reconnect_counter > 10:
442
- logger.critical('Too many reconnects. Exiting.')
443
- sys.exit(1)
444
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
icon.png ADDED
main.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image, ImageDraw, ImageFont, ImageEnhance
2
+ from pilmoji import Pilmoji
3
+ import textwrap
4
+
5
+ BASE_GRADATION_IMAGE = Image.open('base-gd.png')
6
+ BASE_WHITE_IMAGE = Image.open('base.png')
7
+
8
+ ICON = 'icon.png'
9
+
10
+ MPLUS_FONT_16 = ImageFont.truetype('fonts/MPLUSRounded1c-Regular.ttf', size=16)
11
+
12
+ def draw_text(im, ofs, string, font='fonts/MPLUSRounded1c-Regular.ttf', size=16, color=(0,0,0,255), split_len=None, padding=4, auto_expand=False, emojis: list = [], disable_dot_wrap=False):
13
+
14
+ draw = ImageDraw.Draw(im)
15
+ fontObj = ImageFont.truetype(font, size=size)
16
+
17
+ # 改行、句読点(。、.,)で分割した後にさらにワードラップを行う
18
+ pure_lines = []
19
+ pos = 0
20
+ l = ''
21
+
22
+ if not disable_dot_wrap:
23
+ for char in string:
24
+ if char == '\n':
25
+ pure_lines.append(l)
26
+ l = ''
27
+ pos += 1
28
+ elif char == '、' or char == ',':
29
+ pure_lines.append(l + ('、' if char == '、' else ','))
30
+ l = ''
31
+ pos += 1
32
+ elif char == '。' or char == '.':
33
+ pure_lines.append(l + ('。' if char == '。' else '.'))
34
+ l = ''
35
+ pos += 1
36
+ else:
37
+ l += char
38
+ pos += 1
39
+
40
+ if l:
41
+ pure_lines.append(l)
42
+ else:
43
+ pure_lines = string.split('\n')
44
+
45
+ lines = []
46
+
47
+ for line in pure_lines:
48
+ lines.extend(textwrap.wrap(line, width=split_len))
49
+
50
+ dy = 0
51
+
52
+ draw_lines = []
53
+
54
+
55
+ # 計算
56
+ for line in lines:
57
+ tsize = fontObj.getsize(line)
58
+
59
+ ofs_y = ofs[1] + dy
60
+ t_height = tsize[1]
61
+
62
+ x = int(ofs[0] - (tsize[0]/2))
63
+ draw_lines.append((x, ofs_y, line))
64
+ ofs_y += t_height + padding
65
+ dy += t_height + padding
66
+
67
+ # 描画
68
+ adj_y = -30 * (len(draw_lines)-1)
69
+ for dl in draw_lines:
70
+ with Pilmoji(im) as p:
71
+ p.text((dl[0], (adj_y + dl[1])), dl[2], font=fontObj, fill=color, emojis=emojis, emoji_position_offset=(-4, 4))
72
+
73
+ real_y = ofs[1] + adj_y + dy
74
+
75
+ return (0, dy, real_y)
76
+
77
+ content = "これってなんですかね?知らないんですけどwwww でも結局はあれだよね"
78
+ # 引用する
79
+ img = BASE_WHITE_IMAGE.copy()
80
+
81
+ icon = Image.open(ICON)
82
+ icon = icon.resize((720, 720), Image.ANTIALIAS)
83
+ icon = icon.convert('L')
84
+ icon_filtered = ImageEnhance.Brightness(icon)
85
+
86
+ img.paste(icon_filtered.enhance(0.7), (0,0))
87
+
88
+ # 黒グラデ合成
89
+ img.paste(BASE_GRADATION_IMAGE, (0,0), BASE_GRADATION_IMAGE)
90
+
91
+ # テキスト合成
92
+ tx = ImageDraw.Draw(img)
93
+
94
+ base_x = 890
95
+
96
+ # 文章描画
97
+ tsize_t = draw_text(img, (base_x, 270), content, size=45, color=(255,255,255,255), split_len=16, auto_expand=True)
98
+
99
+ # 名前描画
100
+ uname = 'Taka005#6668'
101
+ name_y = tsize_t[2] + 40
102
+ tsize_name = draw_text(img, (base_x, name_y), uname, size=25, color=(255,255,255,255), split_len=25, disable_dot_wrap=True)
103
+
104
+ # ID描画
105
+ id = '000000000000'
106
+ id_y = name_y + tsize_name[1] + 4
107
+ tsize_id = draw_text(img, (base_x, id_y), f'({id})', size=18, color=(180,180,180,255), split_len=45, disable_dot_wrap=True)
108
+
109
+ # クレジット
110
+ tx.text((1125, 694), 'TakasumiBOT#7189', font=MPLUS_FONT_16, fill=(120,120,120,255))
111
+
112
+ img.save('quote.png', quality=95)
modules/emojistore.py DELETED
@@ -1,143 +0,0 @@
1
- import orjson
2
- import sqlite3
3
- import requests
4
- import time
5
- import math
6
- import logging
7
-
8
- CACHE_EXPIRE_TIME = 60 * 60 * 12
9
-
10
- logger = logging.getLogger('EmojiStore')
11
-
12
- class EmojiStore:
13
-
14
- def __init__(self, db, **kwargs):
15
- self.db: sqlite3.Connection = db
16
- self.db.row_factory = sqlite3.Row
17
- cur = self.db.cursor()
18
- cur.execute('CREATE TABLE IF NOT EXISTS emoji_cache(host TEXT, data TEXT, last_updated INTEGER)')
19
- cur.close()
20
-
21
- self.emoji_cache = {}
22
- if kwargs.get('session'):
23
- self.session = kwargs['session']
24
- else:
25
- self.session = requests.Session()
26
- self.session.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'
27
-
28
- def _generate_emoji_url(self, host, emoji: dict):
29
- if emoji.get('url'):
30
- return emoji['url']
31
- else:
32
- # v>=13?
33
- return f'https://{host}/emoji/{emoji["name"]}.webp'
34
-
35
- def _fetch_nodeinfo(self, host):
36
- r = self.session.get(f'https://{host}/.well-known/nodeinfo')
37
- if r.status_code != 200:
38
- logger.getChild('fetch_nodeinfo').error(f'Failed to fetch nodeinfo for {host} (well-known/nodeinfo)')
39
- raise Exception(f'Failed to fetch nodeinfo for {host}')
40
- res = orjson.loads(r.content)
41
- if res.get('links'):
42
- for link in res['links']:
43
- if link['rel'].endswith('nodeinfo.diaspora.software/ns/schema/2.0'):
44
- r2 = self.session.get(link['href'])
45
- if r2.status_code != 200:
46
- logger.getChild('fetch_nodeinfo').error(f'Failed to fetch nodeinfo for {host} (nodeinfo)')
47
- raise Exception(f'Failed to fetch nodeinfo for {host}')
48
- return orjson.loads(r2.content)
49
- logger.getChild('fetch_nodeinfo').error(f'Failed to fetch nodeinfo for {host}')
50
- raise Exception(f'Failed to fetch nodeinfo for {host}')
51
-
52
- def _fetch_emoji_data(self, host):
53
- logger.getChild('fetch_emoji_data').info(f'Fetching emoji data for {host}')
54
- try:
55
- ni = self._fetch_nodeinfo(host)
56
- r = self.session.post(f'https://{host}/api/meta', headers={'Content-Type': 'application/json'}, data=b'{}')
57
- if r.status_code != 200 and r.status_code != 404:
58
- logger.getChild('fetch_emoji_data').error(f'Failed to fetch emoji data for {host} (api/meta)')
59
- raise Exception(f'Failed to fetch emoji data for {host}')
60
- if r.status_code != 404:
61
- meta = orjson.loads(r.content)
62
- v = meta['version'].split('.')
63
- # Misskey v13以降は別エンドポイントに問い合わせ
64
- if ni['software']['name'] == 'misskey' and int(v[0]) >= 13:
65
- r2 = self.session.post(f'https://{host}/api/emojis', headers={'Content-Type': 'application/json'}, data=b'{}')
66
- if r2.status_code != 200:
67
- logger.getChild('fetch_emoji_data').error(f'Failed to fetch emoji data for {host} (Misskey v13)')
68
- raise Exception(f'Failed to fetch emoji data for {host} (Misskey v13)')
69
- return orjson.loads(r2.content)['emojis']
70
- else:
71
- return meta['emojis']
72
- else:
73
- # Mastodon/Pleroma?
74
- r3 = self.session.get(f'https://{host}/api/v1/custom_emojis')
75
- if r3.status_code != 200:
76
- logger.getChild('fetch_emoji_data').error(f'Failed to fetch emoji data for {host} (mastodon, pleroma)')
77
- raise Exception(f'Failed to fetch emoji data for {host} (mastodon, pleroma)')
78
- res = orjson.loads(r3.content)
79
- # Misskey形式に変換
80
- return [{'name': x['shortcode'], 'url': x['static_url'], 'aliases': [''], 'category': ''} for x in res]
81
- except:
82
- return []
83
-
84
- def _download(self, host):
85
- emoji_data = self._fetch_emoji_data(host)
86
- cur = self.db.cursor()
87
- cur.execute('REPLACE INTO emoji_cache(host, data, last_updated) VALUES (?, ?, ?)', (host, orjson.dumps(emoji_data), math.floor(time.time())))
88
- self.db.commit()
89
- self.emoji_cache[host] = emoji_data
90
-
91
- def _load(self, host) -> list:
92
- emojis = []
93
- if host in self.emoji_cache.keys():
94
- emojis = self.emoji_cache[host]
95
- else:
96
- cur = self.db.cursor()
97
- cur.execute('SELECT * FROM emoji_cache WHERE host = ?', (host,))
98
- row = cur.fetchone()
99
- if row is None:
100
- logger.getChild('load').error(f'emoji data not found. fetching')
101
- self._download(host)
102
- return self._load(host)
103
- else:
104
- expire = CACHE_EXPIRE_TIME
105
- # 前回取得失敗してる?
106
- if row['data'] == '[]':
107
- expire = 60 * 5
108
- if math.floor(time.time()) - row['last_updated'] > expire:
109
- logger.getChild('load').error(f'emoji cache expired. refreshing')
110
- self._download(host)
111
- return self._load(host)
112
- self.emoji_cache[host] = orjson.loads(row['data'])
113
- emojis = self.emoji_cache[host]
114
-
115
- return emojis
116
-
117
- # ----------------------
118
-
119
- def refresh(self, host):
120
- self._download(host)
121
-
122
- def find_by_keyword(self, host, k) -> list:
123
- emojis = self._load(host)
124
- res = []
125
- for emoji in emojis:
126
- if k in emoji['name'].lower():
127
- res.append({'name': emoji['name'], 'url': self._generate_emoji_url(host, emoji)})
128
- return res
129
-
130
- def find_by_alias(self, host, t) -> list:
131
- emojis = self._load(host)
132
- res = []
133
- for emoji in emojis:
134
- if t in emoji['aliases']:
135
- res.append({'name': emoji['name'], 'url': self._generate_emoji_url(host, emoji)})
136
- return res
137
-
138
- def get(self, host, name):
139
- emojis = self._load(host)
140
- for emoji in emojis:
141
- if emoji['name'] == name:
142
- return {'name': emoji['name'], 'url': self._generate_emoji_url(host, emoji)}
143
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
quote.png ADDED