this repo has no description

Add RatioFeed

+146
+2
feed_manager.py
··· 6 6 from feeds.homeruns import HomeRunsTeamFeed 7 7 from feeds.norazone_interesting import NoraZoneInteresting 8 8 from feeds.sevendirtywords import SevenDirtyWordsFeed 9 + from feeds.ratio import RatioFeed 9 10 10 11 class FeedManager: 11 12 def __init__(self): ··· 48 49 feed_manager.register(HomeRunsTeamFeed) 49 50 feed_manager.register(NoraZoneInteresting) 50 51 feed_manager.register(SevenDirtyWordsFeed) 52 + feed_manager.register(RatioFeed)
+144
feeds/ratio.py
··· 1 + import logging 2 + 3 + import apsw 4 + import apsw.ext 5 + 6 + from . import BaseFeed 7 + 8 + class RatioFeed(BaseFeed): 9 + FEED_URI = 'at://did:plc:4nsduwlpivpuur4mqkbfvm6a/app.bsky.feed.generator/ratio' 10 + SERVE_FEED_QUERY = """ 11 + with served as ( 12 + select 13 + uri, 14 + create_ts, 15 + ( unixepoch('now') - create_ts ) as age_seconds, 16 + replies, 17 + quoteposts, 18 + likes, 19 + reposts, 20 + ( replies + quoteposts ) / ( likes + reposts ) as ratio, 21 + exp( -1 * ( ( unixepoch('now') - create_ts ) / ( 3600.0 * 16 ) ) ) as decay 22 + from posts 23 + ) 24 + select 25 + *, 26 + ( ratio * decay ) as score 27 + from served 28 + where replies > 15 and ratio > 2.5 29 + order by score desc 30 + limit :limit offset :offset 31 + """ 32 + DELETE_OLD_POSTS_QUERY = """ 33 + delete from posts 34 + where 35 + create_ts < unixepoch('now', '-5 days') 36 + """ 37 + 38 + def __init__(self): 39 + self.db_cnx = apsw.Connection('db/ratio.db') 40 + self.db_cnx.pragma('journal_mode', 'WAL') 41 + self.db_cnx.pragma('wal_autocheckpoint', '0') 42 + 43 + with self.db_cnx: 44 + self.db_cnx.execute(""" 45 + create table if not exists posts ( 46 + uri text, create_ts timestamp, 47 + replies float, likes float, reposts float, quoteposts float 48 + ); 49 + create unique index if not exists uri_idx on posts(uri); 50 + """) 51 + 52 + self.logger = logging.getLogger('feeds.ratio') 53 + 54 + def process_commit(self, commit): 55 + if commit['opType'] != 'c': 56 + return 57 + 58 + subject_uri = None 59 + is_reply = False 60 + is_quotepost = False 61 + 62 + if commit['collection'] in {'app.bsky.feed.like', 'app.bsky.feed.repost'}: 63 + record = commit.get('record') 64 + ts = self.safe_timestamp(record.get('createdAt')).timestamp() 65 + try: 66 + subject_uri = record['subject']['uri'] 67 + except KeyError: 68 + return 69 + elif commit['collection'] == 'app.bsky.feed.post': 70 + record = commit.get('record') 71 + ts = self.safe_timestamp(record.get('createdAt')).timestamp() 72 + if record.get('reply') is not None: 73 + is_reply = True 74 + try: 75 + subject_uri = record['reply']['parent']['uri'] 76 + except KeyError: 77 + return 78 + 79 + # only count non-OP replies 80 + if subject_uri.startswith('at://' + commit['did']): 81 + return 82 + 83 + elif record.get('embed') is not None: 84 + is_quotepost = True 85 + t = record['embed']['$type'] 86 + if t == 'app.bsky.embed.record': 87 + try: 88 + subject_uri = record['embed']['record']['uri'] 89 + except KeyError: 90 + return 91 + elif t == 'app.bsky.embed.recordWithMedia': 92 + try: 93 + subject_uri = record['embed']['record']['record']['uri'] 94 + except KeyError: 95 + return 96 + 97 + if subject_uri is None: 98 + return 99 + 100 + params = { 101 + 'uri': subject_uri, 102 + 'ts': ts, 103 + 'is_reply': int(is_reply), 104 + 'is_like': int(commit['collection'] == 'app.bsky.feed.like'), 105 + 'is_repost': int(commit['collection'] == 'app.bsky.feed.repost'), 106 + 'is_quotepost': int(is_quotepost), 107 + } 108 + 109 + self.transaction_begin(self.db_cnx) 110 + 111 + self.db_cnx.execute(""" 112 + insert into posts(uri, create_ts, replies, likes, reposts, quoteposts) 113 + values (:uri, :ts, 114 + case when :is_reply then 1 else 0 end, 115 + case when :is_like then 1 else 0 end, 116 + case when :is_repost then 1 else 0 end, 117 + case when :is_quotepost then 1 else 0 end) 118 + on conflict(uri) 119 + do update set 120 + replies = replies + case when :is_reply then 1 else 0 end, 121 + likes = likes + case when :is_like then 1 else 0 end, 122 + reposts = reposts + case when :is_repost then 1 else 0 end, 123 + quoteposts = quoteposts + case when :is_quotepost then 1 else 0 end 124 + """, params) 125 + 126 + def delete_old_posts(self): 127 + self.db_cnx.execute(self.DELETE_OLD_POSTS_QUERY) 128 + 129 + def commit_changes(self): 130 + self.logger.debug('committing changes') 131 + self.delete_old_posts() 132 + self.transaction_commit(self.db_cnx) 133 + self.wal_checkpoint(self.db_cnx, 'RESTART') 134 + 135 + def serve_feed(self, limit, offset, langs): 136 + cur = self.db_cnx.execute(self.SERVE_FEED_QUERY, dict(limit=limit, offset=offset)) 137 + return [row[0] for row in cur] 138 + 139 + def serve_feed_debug(self, limit, offset, langs): 140 + bindings = dict(limit=limit, offset=offset) 141 + return apsw.ext.format_query_table( 142 + self.db_cnx, self.SERVE_FEED_QUERY, bindings, 143 + string_sanitize=2, text_width=9999, use_unicode=True 144 + )