def __getitem__(self, n):
return self.data[n]
+class UserInfoFeed(Feed):
+ def __init__(self, client, account):
+ self.client = client
+ self.account = account
+ self.data = [UserInfo(account, client)]
+
+ def start(self):
+ pass
+
+ def min_index(self):
+ return 0
+ def max_index(self):
+ return len(self.data)
+ def __getitem__(self, n):
+ return self.data[n]
+
class EgoFeed(IncrementalServerFeed):
def __init__(self, client):
super().__init__(client, "notifications", {
yield text.Paragraph("Client website: " + client_url)
yield text.BlankLine()
+ def get_account_id(self):
+ return self.account['id']
def get_reply_recipients(self):
yield self.client.fq(self.account['acct'])
for mention in self.mentions:
self.datestamp, self.client.fq(self.account['acct']),
self.account['display_name'], self.ntype, self.content)
+ def get_account_id(self):
+ return self.account['id']
+ def get_reply_recipients(self):
+ yield self.client.fq(self.account['acct'])
+ def get_reply_id(self):
+ return None
+
+class UserInfo:
+ def __init__(self, data, client):
+ self.account = data
+ self.client = client
+
+ self.relationship = None
+ try:
+ for rel in self.client.get("accounts/relationships", id=data['id']):
+ if rel['id'] == data['id']:
+ self.relationship = rel
+ except HTTPError:
+ pass
+
+ hp = text.HTMLParser()
+ hp.feed(data['note'])
+ hp.done()
+ self.bio = []
+ for p in hp.paras:
+ ip = text.IndentedParagraph(2, 2)
+ ip.add_para(p)
+ self.bio.append(ip)
+
+ self.info = []
+ for field in self.account['fields']:
+ hp = text.HTMLParser()
+ hp.feed(field['value'])
+ hp.done()
+ ip = text.IndentedParagraph(2, 4)
+ name = field['name']
+ if not name.endswith(":"):
+ name += ":"
+ ip.add(text.ColouredString(
+ name, 'f' if field['verified_at'] else ' '))
+ it = iter(hp.paras)
+ try:
+ ip.add_para(next(it))
+ except StopIteration:
+ pass
+ self.info.append(ip)
+
+ for p in it:
+ ip = text.IndentedParagraph(4, 4)
+ ip.add_para(p)
+ self.bio.append(ip)
+
+ def text(self):
+ yield text.Paragraph("Account name: " + text.ColouredString(
+ self.client.fq(self.account['acct']), 'f'))
+ url = self.account['url']
+ yield text.Paragraph("On the web: " + text.ColouredString(url, 'u'))
+ yield text.BlankLine()
+ yield text.Paragraph("Display name: " + self.account['display_name'])
+ yield text.Paragraph("Bio:")
+ yield from self.bio
+ yield text.BlankLine()
+ if len(self.info) > 0:
+ yield text.Paragraph("Information:")
+ yield from self.info
+ yield text.BlankLine()
+ flags = list(self.flags_text())
+ if len(flags) > 0:
+ yield from flags
+ yield text.BlankLine()
+ rel = list(self.relationship_text())
+ if len(rel) > 0:
+ yield text.Paragraph("Relationships to this user:")
+ yield from rel
+ yield text.BlankLine()
+ aid = self.account['id']
+ yield text.Paragraph(f"Account id: {aid}")
+ created = noneify(self.account['created_at'])
+ yield text.Paragraph(f"Account created: " + created)
+ posted = noneify(self.account['last_status_at'])
+ yield text.Paragraph(f"Latest post: " + created)
+ n = self.account['statuses_count']
+ yield text.Paragraph(f"Number of posts: {n}")
+ yield text.BlankLine()
+ n = self.account['followers_count']
+ yield text.Paragraph(f"Number of followers: {n}")
+ n = self.account['following_count']
+ yield text.Paragraph(f"Number of users followed: {n}")
+ yield text.BlankLine()
+
+ def flags_text(self):
+ if self.account['locked']:
+ yield text.Paragraph(
+ "This account is " + text.ColouredString("locked", 'r') +
+ " (you can't follow it without its permission).")
+ if self.account.get('suspended'):
+ yield text.Paragraph(text.ColouredString(
+ "This account is suspended.", 'r'))
+ if self.account.get('limited'):
+ yield text.Paragraph(text.ColouredString(
+ "This account is silenced.", 'r'))
+ if self.account['bot']:
+ yield text.Paragraph("This account identifies as a bot.")
+ if self.account['group']:
+ yield text.Paragraph("This account identifies as a group.")
+ moved_to = self.account.get('moved')
+ if moved_to is not None:
+ yield text.Paragraph(
+ text.ColouredString("This account has moved to:", 'r') + " " +
+ text.ColouredString(self.client.fq(moved_to['acct']), 'f'))
+
+ def relationship_text(self):
+ if self.relationship is None:
+ return
+ if self.account['id'] == self.client.account_id:
+ yield text.IndentedParagraph(2, 4, text.ColouredString(
+ "You are this user!",
+ " ___ "))
+ if self.relationship['following']:
+ if self.relationship['showing_reblogs']:
+ yield text.IndentedParagraph(2, 4, text.ColouredString(
+ "You follow this user.", 'f'))
+ else:
+ yield text.IndentedParagraph(2, 4, text.ColouredString(
+ "You follow this user (but without boosts).", 'f'))
+ if self.relationship['followed_by']:
+ yield text.IndentedParagraph(2, 4, text.ColouredString(
+ "This user follows you.", 'f'))
+ if self.relationship['requested']:
+ yield text.IndentedParagraph(2, 4, text.ColouredString(
+ "This user has requested to follow you!", 'F'))
+ if self.relationship['notifying']:
+ yield text.IndentedParagraph(
+ 2, 4, "You have enabled notifications for this user.")
+ if self.relationship['blocking']:
+ yield text.IndentedParagraph(2, 4, text.ColouredString(
+ "You have blocked this user.", 'r'))
+ if self.relationship['blocked_by']:
+ yield text.IndentedParagraph(2, 4, text.ColouredString(
+ "This user has blocked you.", 'r'))
+ if self.relationship['muting']:
+ yield text.IndentedParagraph(2, 4, text.ColouredString(
+ "You have muted this user.", 'r'))
+ if self.relationship['muting_notifications']:
+ yield text.IndentedParagraph(2, 4, text.ColouredString(
+ "You have muted notifications from this user.", 'r'))
+ if self.relationship['domain_blocking']:
+ yield text.IndentedParagraph(
+ 2, 4, "You have blocked this user's domain.")
+
+ def get_account_id(self):
+ return self.account['id']
def get_reply_recipients(self):
yield self.client.fq(self.account['acct'])
def get_reply_id(self):
import signal
import sys
import threading
+import time
import unittest
import client
if ch == ctrl('['):
if self.activity_stack[-1] is not self.escape_menu:
+ self.escape_menu.activation = time.monotonic()
self.activity_stack.append(self.escape_menu)
elif ch == curses.KEY_RESIZE:
self.scr_h, self.scr_w = self.scr.getmaxyx()
def push_to(self, activity):
self.cc.activity_stack.append(activity)
+ def optionally_chain_to(self, activity):
+ if self.cc.activity_stack[-1] is self:
+ self.chain_to(activity)
+ else:
+ self.push_to(activity)
+
class Menu(Activity):
status_extra_text = None
self.title = text.ColouredString(
"Utilities [ESC]",
"HHHHHHHHHHHKKKH")
+ self.items.append(text.MenuKeypressLine(
+ 'E', text.ColouredString("Examine User",
+ "K ")))
+ self.items.append(text.MenuKeypressLine(
+ 'Y', text.ColouredString("Examine Yourself",
+ " K ")))
+ self.items.append(text.BlankLine())
self.items.append(text.MenuKeypressLine(
'L', text.ColouredString("Logs menu",
"K ")))
'X', text.ColouredString("EXit Mastodonochrome",
" K ")))
+ self.activation = None
+
+ def recently_activated(self):
+ return (self.activation is not None and
+ time.monotonic() - self.activation < 0.5)
+
+ def prompt_to(self, prompt):
+ if self.recently_activated():
+ self.chain_to(prompt)
+ else:
+ self.push_to(prompt)
+
def handle_key(self, ch):
if ch in {'r', 'R'}:
self.chain_to(self.cc.mentions_timeline)
self.chain_to(self.cc.exit_menu)
elif ch in {'g', 'G'}:
self.cc.activity_stack[:] = [self.cc.main_menu]
+ elif ch in {'e', 'E'}:
+ self.prompt_to(BottomLinePrompt(
+ self.cc, self.got_user_to_examine,
+ "Examine User: "))
+ elif ch in {'y', 'Y'}:
+ self.chain_to(UserInfoFile.by_id(self.cc, self.cc.account_id))
else:
return super().handle_key(ch)
+ def got_user_to_examine(self, text):
+ text = text.strip().lstrip("@")
+ if text == "":
+ return
+
+ try:
+ acct = self.cc.get("accounts/lookup", acct=text)
+ except client.HTTPError:
+ curses.beep() # FIXME: better error report?
+ return
+ if "id" not in acct:
+ curses.beep() # FIXME: better error report?
+ return
+ self.optionally_chain_to(UserInfoFile(self.cc, acct))
+
class LogMenu(Menu):
def __init__(self, cc):
super().__init__(cc)
self.thread_mode()
elif ch in {'i', 'I'}:
self.info_mode()
+ elif ch in {'e', 'E'}:
+ self.examine_mode()
elif ch in {'\\'}:
self.search_direction = -1
self.push_to(BottomLinePrompt(
elif (self.select_type == 'info' and
ch in {' '}):
self.info_complete(False)
+ elif (self.select_type == 'examine' and
+ ch in {' '}):
+ self.examine_complete(False)
def got_search_text(self, text):
if len(text) != 0:
pass # not supported
def thread_mode(self):
pass # not supported
+ def info_mode(self):
+ pass # not supported
+ def examine_mode(self):
+ pass # not supported
class ObjectFile(File):
def __init__(self, cc, constructor, feed, title):
elif self.select_type == 'thread':
sl.keys.append(('SPACE', 'Show Thread Context'))
sl.keys.append(('F', 'Show Full Thread'))
+ elif self.select_type == 'examine':
+ sl.keys.append(('SPACE', 'Examine'))
elif self.select_type == 'info':
sl.keys.append(('SPACE', 'Show Post Info'))
sl.keys.append(('-', None))
self.mode = 'select'
self.select_type = 'info'
self.select_target = self.index_by_line[self.linepos-1]
+ def examine_mode(self):
+ if self.items_have_authors:
+ self.mode = 'select'
+ self.select_type = 'examine'
+ self.select_target = self.index_by_line[self.linepos-1]
def prev_select_target(self):
self.select_target = max(self.minpos, self.select_target-1)
def next_select_target(self):
reply_id = target.get_reply_id()
self.push_to(StatusInfoFile(self.cc, reply_id))
+ def examine_complete(self, full):
+ self.mode = 'normal'
+ target = self.statuses[self.select_target]
+ account_id = target.get_account_id()
+ self.push_to(UserInfoFile.by_id(self.cc, account_id))
+
def move_to(self, pos):
old_linepos = self.linepos
self.linepos = pos
super().__init__(
cc, lambda x,cc:x, client.StatusInfoFeed(cc, self.data), title)
+class UserInfoFile(ObjectFile):
+ items_are_statuses = False
+ items_have_authors = True
+ def __init__(self, cc, account):
+ self.account = account
+ name = cc.fq(account['acct'])
+ title = text.ColouredString(f"Information about user {name}", 'H')
+ super().__init__(
+ cc, lambda x,cc:x, client.UserInfoFeed(cc, self.account), title)
+
+ @classmethod
+ def by_id(cls, cc, account_id):
+ account = cc.get("accounts/" + account_id)
+ return cls(cc, account)
+
class EditorCommon:
# Common editing operations between the post editor and the line editor.
# Expects self.text to be the editor buffer, and self.point to be
self.ctrl_k_paste_buffer = self.text[self.point:]
self.text = (self.text[:self.point])
elif ch in {'\r', '\n'}:
- self.callback(self.text)
self.chain_to(self.parent_activity)
+ self.callback(self.text)
else:
self.handle_common_editing_keys(ch)