File size: 6,352 Bytes
a28bbcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import orjson
import sqlite3
import requests
import time
import math
import logging

CACHE_EXPIRE_TIME = 60 * 60 * 12

logger = logging.getLogger('EmojiStore')

class EmojiStore:

    def __init__(self, db, **kwargs):
        self.db: sqlite3.Connection = db
        self.db.row_factory = sqlite3.Row
        cur = self.db.cursor()
        cur.execute('CREATE TABLE IF NOT EXISTS emoji_cache(host TEXT, data TEXT, last_updated INTEGER)')
        cur.close()

        self.emoji_cache = {}
        if kwargs.get('session'):
            self.session = kwargs['session']
        else:
            self.session = requests.Session()
            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'
    
    def _generate_emoji_url(self, host, emoji: dict):
        if emoji.get('url'):
            return emoji['url']
        else:
            # v>=13?
            return f'https://{host}/emoji/{emoji["name"]}.webp'
    
    def _fetch_nodeinfo(self, host):
        r = self.session.get(f'https://{host}/.well-known/nodeinfo')
        if r.status_code != 200:
            logger.getChild('fetch_nodeinfo').error(f'Failed to fetch nodeinfo for {host} (well-known/nodeinfo)')
            raise Exception(f'Failed to fetch nodeinfo for {host}')
        res = orjson.loads(r.content)
        if res.get('links'):
            for link in res['links']:
                if link['rel'].endswith('nodeinfo.diaspora.software/ns/schema/2.0'):
                    r2 = self.session.get(link['href'])
                    if r2.status_code != 200:
                        logger.getChild('fetch_nodeinfo').error(f'Failed to fetch nodeinfo for {host} (nodeinfo)')
                        raise Exception(f'Failed to fetch nodeinfo for {host}')
                    return orjson.loads(r2.content)
        logger.getChild('fetch_nodeinfo').error(f'Failed to fetch nodeinfo for {host}')
        raise Exception(f'Failed to fetch nodeinfo for {host}')
    
    def _fetch_emoji_data(self, host):
        logger.getChild('fetch_emoji_data').info(f'Fetching emoji data for {host}')
        try:
            ni = self._fetch_nodeinfo(host)
            r = self.session.post(f'https://{host}/api/meta', headers={'Content-Type': 'application/json'}, data=b'{}')
            if r.status_code != 200 and r.status_code != 404:
                logger.getChild('fetch_emoji_data').error(f'Failed to fetch emoji data for {host} (api/meta)')
                raise Exception(f'Failed to fetch emoji data for {host}')
            if r.status_code != 404:
                meta = orjson.loads(r.content)
                v = meta['version'].split('.')
                # Misskey v13以降は別エンドポイントに問い合わせ
                if ni['software']['name'] == 'misskey' and int(v[0]) >= 13:
                    r2 = self.session.post(f'https://{host}/api/emojis', headers={'Content-Type': 'application/json'}, data=b'{}')
                    if r2.status_code != 200:
                        logger.getChild('fetch_emoji_data').error(f'Failed to fetch emoji data for {host} (Misskey v13)')
                        raise Exception(f'Failed to fetch emoji data for {host} (Misskey v13)')
                    return orjson.loads(r2.content)['emojis']
                else:
                    return meta['emojis']
            else:
                # Mastodon/Pleroma?
                r3 = self.session.get(f'https://{host}/api/v1/custom_emojis')
                if r3.status_code != 200:
                    logger.getChild('fetch_emoji_data').error(f'Failed to fetch emoji data for {host} (mastodon, pleroma)')
                    raise Exception(f'Failed to fetch emoji data for {host} (mastodon, pleroma)')
                res = orjson.loads(r3.content)
                # Misskey形式に変換
                return [{'name': x['shortcode'], 'url': x['static_url'], 'aliases': [''], 'category': ''} for x in res]
        except:
            return []
    
    def _download(self, host):
        emoji_data = self._fetch_emoji_data(host)
        cur = self.db.cursor()
        cur.execute('REPLACE INTO emoji_cache(host, data, last_updated) VALUES (?, ?, ?)', (host, orjson.dumps(emoji_data), math.floor(time.time())))
        self.db.commit()
        self.emoji_cache[host] = emoji_data

    def _load(self, host) -> list:
        emojis = []
        if host in self.emoji_cache.keys():
            emojis = self.emoji_cache[host]
        else:
            cur = self.db.cursor()
            cur.execute('SELECT * FROM emoji_cache WHERE host = ?', (host,))
            row = cur.fetchone()
            if row is None:
                logger.getChild('load').error(f'emoji data not found. fetching')
                self._download(host)
                return self._load(host)
            else:
                expire = CACHE_EXPIRE_TIME
                # 前回取得失敗してる?
                if row['data'] == '[]':
                    expire = 60 * 5
                if math.floor(time.time()) - row['last_updated'] > expire:
                    logger.getChild('load').error(f'emoji cache expired. refreshing')
                    self._download(host)
                    return self._load(host)
            self.emoji_cache[host] = orjson.loads(row['data'])
            emojis = self.emoji_cache[host]
        
        return emojis
    
    # ----------------------

    def refresh(self, host):
        self._download(host)
    
    def find_by_keyword(self, host, k) -> list:
        emojis = self._load(host)
        res = []
        for emoji in emojis:
            if k in emoji['name'].lower():
                res.append({'name': emoji['name'], 'url': self._generate_emoji_url(host, emoji)})
        return res
    
    def find_by_alias(self, host, t) -> list:
        emojis = self._load(host)
        res = []
        for emoji in emojis:
            if t in emoji['aliases']:
                res.append({'name': emoji['name'], 'url': self._generate_emoji_url(host, emoji)})
        return res
    
    def get(self, host, name):
        emojis = self._load(host)
        for emoji in emojis:
            if emoji['name'] == name:
                return {'name': emoji['name'], 'url': self._generate_emoji_url(host, emoji)}
        return None