From 2cfe56bbbe47c9bf8aa87c48324091e90623d43f Mon Sep 17 00:00:00 2001 From: mathieui Date: Wed, 3 Feb 2021 21:04:36 +0100 Subject: Remove activity/mood/gaming/tune from poezio core rationale: probably 0.1% of poezio users have ever used the commands, and a very small number of users are using the PEP events like that. It is better to unclutter the poezio source and add it as a plugin, with less tight coupling. --- poezio/connection.py | 12 --- poezio/contact.py | 14 +-- poezio/core/commands.py | 72 --------------- poezio/core/completions.py | 28 ------ poezio/core/core.py | 50 ----------- poezio/core/handlers.py | 173 ------------------------------------ poezio/pep.py | 207 ------------------------------------------- poezio/tabs/rostertab.py | 9 -- poezio/windows/roster_win.py | 46 ---------- 9 files changed, 9 insertions(+), 602 deletions(-) delete mode 100644 poezio/pep.py (limited to 'poezio') diff --git a/poezio/connection.py b/poezio/connection.py index bbaf2e69..602a9f36 100644 --- a/poezio/connection.py +++ b/poezio/connection.py @@ -165,21 +165,9 @@ class Connection(slixmpp.ClientXMPP): self.register_plugin('xep_0198') self.register_plugin('xep_0199') - if config.get('enable_user_tune'): - self.register_plugin('xep_0118') - if config.get('enable_user_nick'): self.register_plugin('xep_0172') - if config.get('enable_user_mood'): - self.register_plugin('xep_0107') - - if config.get('enable_user_activity'): - self.register_plugin('xep_0108') - - if config.get('enable_user_gaming'): - self.register_plugin('xep_0196') - if config.get('send_poezio_info'): info = {'name': 'poezio', 'version': options.custom_version} if config.get('send_os_info'): diff --git a/poezio/contact.py b/poezio/contact.py index 3330a2a6..063405c9 100644 --- a/poezio/contact.py +++ b/poezio/contact.py @@ -11,7 +11,14 @@ the roster. from collections import defaultdict import logging -from typing import Dict, Iterator, List, Optional, Union +from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + Union, +) from slixmpp import InvalidJID, JID @@ -73,10 +80,7 @@ class Contact: self._name = '' self.avatar = None self.error = None - self.tune: Dict[str, str] = {} - self.gaming: Dict[str, str] = {} - self.mood = '' - self.activity = '' + self.rich_presence: Dict[str, Any] = defaultdict(lambda: None) @property def groups(self) -> List[str]: diff --git a/poezio/core/commands.py b/poezio/core/commands.py index 964bd0a7..ebd8f130 100644 --- a/poezio/core/commands.py +++ b/poezio/core/commands.py @@ -15,7 +15,6 @@ from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from poezio import common -from poezio import pep from poezio import tabs from poezio import multiuserchat as muc from poezio.bookmarks import Bookmark @@ -938,71 +937,6 @@ class CommandCore: if status else '') self.core.information(msg, 'Info') - @command_args_parser.quoted(0, 2) - def mood(self, args): - """ - /mood [ [text]] - """ - if not args: - return self.core.xmpp.plugin['xep_0107'].stop() - - mood = args[0] - if mood not in pep.MOODS: - return self.core.information( - '%s is not a correct value for a mood.' % mood, 'Error') - if len(args) == 2: - text = args[1] - else: - text = None - self.core.xmpp.plugin['xep_0107'].publish_mood( - mood, text, callback=dumb_callback) - - @command_args_parser.quoted(0, 3) - def activity(self, args): - """ - /activity [ [specific] [text]] - """ - length = len(args) - if not length: - return self.core.xmpp.plugin['xep_0108'].stop() - - general = args[0] - if general not in pep.ACTIVITIES: - return self.core.information( - '%s is not a correct value for an activity' % general, 'Error') - specific = None - text = None - if length == 2: - if args[1] in pep.ACTIVITIES[general]: - specific = args[1] - else: - text = args[1] - elif length == 3: - specific = args[1] - text = args[2] - if specific and specific not in pep.ACTIVITIES[general]: - return self.core.information( - '%s is not a correct value ' - 'for an activity' % specific, 'Error') - self.core.xmpp.plugin['xep_0108'].publish_activity( - general, specific, text, callback=dumb_callback) - - @command_args_parser.quoted(0, 2) - def gaming(self, args): - """ - /gaming [ [server address]] - """ - if not args: - return self.core.xmpp.plugin['xep_0196'].stop() - - name = args[0] - if len(args) > 1: - address = args[1] - else: - address = None - return self.core.xmpp.plugin['xep_0196'].publish_gaming( - name=name, server_address=address, callback=dumb_callback) - @command_args_parser.quoted(2, 1, [None]) async def invite(self, args): """/invite [reason]""" @@ -1200,12 +1134,6 @@ class CommandCore: return msg = args[0] - if config.get('enable_user_mood'): - self.core.xmpp.plugin['xep_0107'].stop() - if config.get('enable_user_activity'): - self.core.xmpp.plugin['xep_0108'].stop() - if config.get('enable_user_gaming'): - self.core.xmpp.plugin['xep_0196'].stop() self.core.save_config() self.core.plugin_manager.disable_plugins() self.core.xmpp.add_event_handler( diff --git a/poezio/core/completions.py b/poezio/core/completions.py index 98ca9ba0..88ed57b3 100644 --- a/poezio/core/completions.py +++ b/poezio/core/completions.py @@ -13,7 +13,6 @@ from functools import reduce from slixmpp import JID from poezio import common -from poezio import pep from poezio import tabs from poezio import xdg from poezio.common import safeJID @@ -318,33 +317,6 @@ class CompletionCore: comp = sorted(onlines) + sorted(offlines) return Completion(the_input.new_completion, comp, n, quotify=True) - def activity(self, the_input): - """Completion for /activity""" - n = the_input.get_argument_position(quoted=True) - args = common.shell_split(the_input.text) - if n == 1: - return Completion( - the_input.new_completion, - sorted(pep.ACTIVITIES.keys()), - n, - quotify=True) - elif n == 2: - if args[1] in pep.ACTIVITIES: - l = list(pep.ACTIVITIES[args[1]]) - l.remove('category') - l.sort() - return Completion(the_input.new_completion, l, n, quotify=True) - - def mood(self, the_input): - """Completion for /mood""" - n = the_input.get_argument_position(quoted=True) - if n == 1: - return Completion( - the_input.new_completion, - sorted(pep.MOODS.keys()), - 1, - quotify=True) - def last_activity(self, the_input): """ Completion for /last_activity diff --git a/poezio/core/core.py b/poezio/core/core.py index 703319c5..efab8810 100644 --- a/poezio/core/core.py +++ b/poezio/core/core.py @@ -297,22 +297,9 @@ class Core: self.handler.on_vcard_avatar) self.xmpp.add_event_handler("avatar_metadata_publish", self.handler.on_0084_avatar) - if config.get('enable_user_tune'): - self.xmpp.add_event_handler("user_tune_publish", - self.handler.on_tune_event) if config.get('enable_user_nick'): self.xmpp.add_event_handler("user_nick_publish", self.handler.on_nick_received) - if config.get('enable_user_mood'): - self.xmpp.add_event_handler("user_mood_publish", - self.handler.on_mood_event) - if config.get('enable_user_activity'): - self.xmpp.add_event_handler("user_activity_publish", - self.handler.on_activity_event) - if config.get('enable_user_gaming'): - self.xmpp.add_event_handler("user_gaming_publish", - self.handler.on_gaming_event) - all_stanzas = Callback('custom matcher', connection.MatchAll(None), self.handler.incoming_stanza) self.xmpp.register_handler(all_stanzas) @@ -525,12 +512,6 @@ class Core: } log.error("%s received. Exiting…", signals[sig]) - if config.get('enable_user_mood'): - self.xmpp.plugin['xep_0107'].stop() - if config.get('enable_user_activity'): - self.xmpp.plugin['xep_0108'].stop() - if config.get('enable_user_gaming'): - self.xmpp.plugin['xep_0196'].stop() self.plugin_manager.disable_plugins() self.disconnect('%s received' % signals.get(sig)) self.xmpp.add_event_handler("disconnected", self.exit, disposable=True) @@ -1693,37 +1674,6 @@ class Core: for command in get_commands(self.command, self.completion, self.plugin_manager): self.register_command(**command) - if config.get('enable_user_activity'): - self.register_command( - 'activity', - self.command.activity, - usage='[ [specific] [text]]', - desc='Send your current activity to your contacts ' - '(use the completion). Nothing means ' - '"stop broadcasting an activity".', - shortdesc='Send your activity.', - completion=self.completion.activity) - if config.get('enable_user_mood'): - self.register_command( - 'mood', - self.command.mood, - usage='[ [text]]', - desc='Send your current mood to your contacts ' - '(use the completion). Nothing means ' - '"stop broadcasting a mood".', - shortdesc='Send your mood.', - completion=self.completion.mood) - if config.get('enable_user_gaming'): - self.register_command( - 'gaming', - self.command.gaming, - usage='[ [server address]]', - desc='Send your current gaming activity to ' - 'your contacts. Nothing means "stop ' - 'broadcasting a gaming activity".', - shortdesc='Send your gaming activity.', - completion=None) - def check_blocking(self, features: List[str]): if 'urn:xmpp:blocking' in features and not self.xmpp.anon: self.register_command( diff --git a/poezio/core/handlers.py b/poezio/core/handlers.py index 1d5dcffe..ec714915 100644 --- a/poezio/core/handlers.py +++ b/poezio/core/handlers.py @@ -27,7 +27,6 @@ from xml.etree import ElementTree as ET from poezio import common from poezio import fixes -from poezio import pep from poezio import tabs from poezio import xhtml from poezio import multiuserchat as muc @@ -539,178 +538,6 @@ class HandlerCore: else: contact.name = '' - def on_gaming_event(self, message): - """ - Called when a pep notification for user gaming - is received - """ - contact = roster[message['from'].bare] - if not contact: - return - item = message['pubsub_event']['items']['item'] - old_gaming = contact.gaming - xml_node = item.xml.find('{urn:xmpp:gaming:0}game') - # list(xml_node) checks whether there are children or not. - if xml_node is not None and list(xml_node): - item = item['gaming'] - # only name and server_address are used for now - contact.gaming = { - 'character_name': item['character_name'], - 'character_profile': item['character_profile'], - 'name': item['name'], - 'level': item['level'], - 'uri': item['uri'], - 'server_name': item['server_name'], - 'server_address': item['server_address'], - } - else: - contact.gaming = {} - - if contact.gaming: - logger.log_roster_change( - contact.bare_jid, 'is playing %s' % - (common.format_gaming_string(contact.gaming))) - - if old_gaming != contact.gaming and config.get_by_tabname( - 'display_gaming_notifications', contact.bare_jid): - if contact.gaming: - self.core.information( - '%s is playing %s' % (contact.bare_jid, - common.format_gaming_string( - contact.gaming)), 'Gaming') - else: - self.core.information(contact.bare_jid + ' stopped playing.', - 'Gaming') - - def on_mood_event(self, message): - """ - Called when a pep notification for a user mood - is received. - """ - contact = roster[message['from'].bare] - if not contact: - return - roster.modified() - item = message['pubsub_event']['items']['item'] - old_mood = contact.mood - xml_node = item.xml.find('{http://jabber.org/protocol/mood}mood') - # list(xml_node) checks whether there are children or not. - if xml_node is not None and list(xml_node): - mood = item['mood']['value'] - if mood: - mood = pep.MOODS.get(mood, mood) - text = item['mood']['text'] - if text: - mood = '%s (%s)' % (mood, text) - contact.mood = mood - else: - contact.mood = '' - else: - contact.mood = '' - - if contact.mood: - logger.log_roster_change(contact.bare_jid, - 'has now the mood: %s' % contact.mood) - - if old_mood != contact.mood and config.get_by_tabname( - 'display_mood_notifications', contact.bare_jid): - if contact.mood: - self.core.information( - 'Mood from ' + contact.bare_jid + ': ' + contact.mood, - 'Mood') - else: - self.core.information( - contact.bare_jid + ' stopped having their mood.', 'Mood') - - def on_activity_event(self, message): - """ - Called when a pep notification for a user activity - is received. - """ - contact = roster[message['from'].bare] - if not contact: - return - roster.modified() - item = message['pubsub_event']['items']['item'] - old_activity = contact.activity - xml_node = item.xml.find('{http://jabber.org/protocol/activity}activity') - # list(xml_node) checks whether there are children or not. - if xml_node is not None and list(xml_node): - try: - activity = item['activity']['value'] - except ValueError: - return - if activity[0]: - general = pep.ACTIVITIES.get(activity[0]) - s = general['category'] - if activity[1]: - s = s + '/' + general.get(activity[1], 'other') - text = item['activity']['text'] - if text: - s = '%s (%s)' % (s, text) - contact.activity = s - else: - contact.activity = '' - else: - contact.activity = '' - - if contact.activity: - logger.log_roster_change( - contact.bare_jid, 'has now the activity %s' % contact.activity) - - if old_activity != contact.activity and config.get_by_tabname( - 'display_activity_notifications', contact.bare_jid): - if contact.activity: - self.core.information( - 'Activity from ' + contact.bare_jid + ': ' + - contact.activity, 'Activity') - else: - self.core.information( - contact.bare_jid + ' stopped doing their activity.', - 'Activity') - - def on_tune_event(self, message): - """ - Called when a pep notification for a user tune - is received - """ - contact = roster[message['from'].bare] - if not contact: - return - roster.modified() - item = message['pubsub_event']['items']['item'] - old_tune = contact.tune - xml_node = item.xml.find('{http://jabber.org/protocol/tune}tune') - # list(xml_node) checks whether there are children or not. - if xml_node is not None and list(xml_node): - item = item['tune'] - contact.tune = { - 'artist': item['artist'], - 'length': item['length'], - 'rating': item['rating'], - 'source': item['source'], - 'title': item['title'], - 'track': item['track'], - 'uri': item['uri'] - } - else: - contact.tune = {} - - if contact.tune: - logger.log_roster_change( - message['from'].bare, 'is now listening to %s' % - common.format_tune_string(contact.tune)) - - if old_tune != contact.tune and config.get_by_tabname( - 'display_tune_notifications', contact.bare_jid): - if contact.tune: - self.core.information( - 'Tune from ' + message['from'].bare + ': ' + - common.format_tune_string(contact.tune), 'Tune') - else: - self.core.information( - contact.bare_jid + ' stopped listening to music.', 'Tune') - def on_groupchat_message(self, message): """ Triggered whenever a message is received from a multi-user chat room. diff --git a/poezio/pep.py b/poezio/pep.py deleted file mode 100644 index dde97ed6..00000000 --- a/poezio/pep.py +++ /dev/null @@ -1,207 +0,0 @@ -""" -Collection of mappings for PEP moods/activities -extracted directly from the XEP -""" - -from typing import Dict - -MOODS: Dict[str, str] = { - 'afraid': 'Afraid', - 'amazed': 'Amazed', - 'angry': 'Angry', - 'amorous': 'Amorous', - 'annoyed': 'Annoyed', - 'anxious': 'Anxious', - 'aroused': 'Aroused', - 'ashamed': 'Ashamed', - 'bored': 'Bored', - 'brave': 'Brave', - 'calm': 'Calm', - 'cautious': 'Cautious', - 'cold': 'Cold', - 'confident': 'Confident', - 'confused': 'Confused', - 'contemplative': 'Contemplative', - 'contented': 'Contented', - 'cranky': 'Cranky', - 'crazy': 'Crazy', - 'creative': 'Creative', - 'curious': 'Curious', - 'dejected': 'Dejected', - 'depressed': 'Depressed', - 'disappointed': 'Disappointed', - 'disgusted': 'Disgusted', - 'dismayed': 'Dismayed', - 'distracted': 'Distracted', - 'embarrassed': 'Embarrassed', - 'envious': 'Envious', - 'excited': 'Excited', - 'flirtatious': 'Flirtatious', - 'frustrated': 'Frustrated', - 'grumpy': 'Grumpy', - 'guilty': 'Guilty', - 'happy': 'Happy', - 'hopeful': 'Hopeful', - 'hot': 'Hot', - 'humbled': 'Humbled', - 'humiliated': 'Humiliated', - 'hungry': 'Hungry', - 'hurt': 'Hurt', - 'impressed': 'Impressed', - 'in_awe': 'In awe', - 'in_love': 'In love', - 'indignant': 'Indignant', - 'interested': 'Interested', - 'intoxicated': 'Intoxicated', - 'invincible': 'Invincible', - 'jealous': 'Jealous', - 'lonely': 'Lonely', - 'lucky': 'Lucky', - 'mean': 'Mean', - 'moody': 'Moody', - 'nervous': 'Nervous', - 'neutral': 'Neutral', - 'offended': 'Offended', - 'outraged': 'Outraged', - 'playful': 'Playful', - 'proud': 'Proud', - 'relaxed': 'Relaxed', - 'relieved': 'Relieved', - 'remorseful': 'Remorseful', - 'restless': 'Restless', - 'sad': 'Sad', - 'sarcastic': 'Sarcastic', - 'serious': 'Serious', - 'shocked': 'Shocked', - 'shy': 'Shy', - 'sick': 'Sick', - 'sleepy': 'Sleepy', - 'spontaneous': 'Spontaneous', - 'stressed': 'Stressed', - 'strong': 'Strong', - 'surprised': 'Surprised', - 'thankful': 'Thankful', - 'thirsty': 'Thirsty', - 'tired': 'Tired', - 'undefined': 'Undefined', - 'weak': 'Weak', - 'worried': 'Worried' -} - -ACTIVITIES: Dict[str, Dict[str, str]] = { - 'doing_chores': { - 'category': 'Doing_chores', - 'buying_groceries': 'Buying groceries', - 'cleaning': 'Cleaning', - 'cooking': 'Cooking', - 'doing_maintenance': 'Doing maintenance', - 'doing_the_dishes': 'Doing the dishes', - 'doing_the_laundry': 'Doing the laundry', - 'gardening': 'Gardening', - 'running_an_errand': 'Running an errand', - 'walking_the_dog': 'Walking the dog', - 'other': 'Other', - }, - 'drinking': { - 'category': 'Drinking', - 'having_a_beer': 'Having a beer', - 'having_coffee': 'Having coffee', - 'having_tea': 'Having tea', - 'other': 'Other', - }, - 'eating': { - 'category': 'Eating', - 'having_breakfast': 'Having breakfast', - 'having_a_snack': 'Having a snack', - 'having_dinner': 'Having dinner', - 'having_lunch': 'Having lunch', - 'other': 'Other', - }, - 'exercising': { - 'category': 'Exercising', - 'cycling': 'Cycling', - 'dancing': 'Dancing', - 'hiking': 'Hiking', - 'jogging': 'Jogging', - 'playing_sports': 'Playing sports', - 'running': 'Running', - 'skiing': 'Skiing', - 'swimming': 'Swimming', - 'working_out': 'Working out', - 'other': 'Other', - }, - 'grooming': { - 'category': 'Grooming', - 'at_the_spa': 'At the spa', - 'brushing_teeth': 'Brushing teeth', - 'getting_a_haircut': 'Getting a haircut', - 'shaving': 'Shaving', - 'taking_a_bath': 'Taking a bath', - 'taking_a_shower': 'Taking a shower', - 'other': 'Other', - }, - 'having_appointment': { - 'category': 'Having appointment', - 'other': 'Other', - }, - 'inactive': { - 'category': 'Inactive', - 'day_off': 'Day_off', - 'hanging_out': 'Hanging out', - 'hiding': 'Hiding', - 'on_vacation': 'On vacation', - 'praying': 'Praying', - 'scheduled_holiday': 'Scheduled holiday', - 'sleeping': 'Sleeping', - 'thinking': 'Thinking', - 'other': 'Other', - }, - 'relaxing': { - 'category': 'Relaxing', - 'fishing': 'Fishing', - 'gaming': 'Gaming', - 'going_out': 'Going out', - 'partying': 'Partying', - 'reading': 'Reading', - 'rehearsing': 'Rehearsing', - 'shopping': 'Shopping', - 'smoking': 'Smoking', - 'socializing': 'Socializing', - 'sunbathing': 'Sunbathing', - 'watching_a_movie': 'Watching a movie', - 'watching_tv': 'Watching tv', - 'other': 'Other', - }, - 'talking': { - 'category': 'Talking', - 'in_real_life': 'In real life', - 'on_the_phone': 'On the phone', - 'on_video_phone': 'On video phone', - 'other': 'Other', - }, - 'traveling': { - 'category': 'Traveling', - 'commuting': 'Commuting', - 'driving': 'Driving', - 'in_a_car': 'In a car', - 'on_a_bus': 'On a bus', - 'on_a_plane': 'On a plane', - 'on_a_train': 'On a train', - 'on_a_trip': 'On a trip', - 'walking': 'Walking', - 'cycling': 'Cycling', - 'other': 'Other', - }, - 'undefined': { - 'category': 'Undefined', - 'other': 'Other', - }, - 'working': { - 'category': 'Working', - 'coding': 'Coding', - 'in_a_meeting': 'In a meeting', - 'writing': 'Writing', - 'studying': 'Studying', - 'other': 'Other', - } -} diff --git a/poezio/tabs/rostertab.py b/poezio/tabs/rostertab.py index ad57a9fd..5d2e148c 100644 --- a/poezio/tabs/rostertab.py +++ b/poezio/tabs/rostertab.py @@ -1012,15 +1012,6 @@ class RosterInfoTab(Tab): '%s connected resource%s' % (len(cont), '' if len(cont) == 1 else 's')) acc.append('Current status: %s' % res.status) - if cont.tune: - acc.append('Tune: %s' % common.format_tune_string(cont.tune)) - if cont.mood: - acc.append('Mood: %s' % cont.mood) - if cont.activity: - acc.append('Activity: %s' % cont.activity) - if cont.gaming: - acc.append( - 'Game: %s' % (common.format_gaming_string(cont.gaming))) msg = '\n'.join(acc) elif isinstance(selected_row, Resource): res = selected_row diff --git a/poezio/windows/roster_win.py b/poezio/windows/roster_win.py index c4a1c30b..74de7eef 100644 --- a/poezio/windows/roster_win.py +++ b/poezio/windows/roster_win.py @@ -268,14 +268,6 @@ class RosterWin(Win): added += len(theme.CHAR_ROSTER_ASKED) if show_s2s_errors and contact.error: added += len(theme.CHAR_ROSTER_ERROR) - if contact.tune: - added += len(theme.CHAR_ROSTER_TUNE) - if contact.mood: - added += len(theme.CHAR_ROSTER_MOOD) - if contact.activity: - added += len(theme.CHAR_ROSTER_ACTIVITY) - if contact.gaming: - added += len(theme.CHAR_ROSTER_GAMING) if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', 'none'): added += len( @@ -309,18 +301,6 @@ class RosterWin(Win): if show_s2s_errors and contact.error: self.addstr(theme.CHAR_ROSTER_ERROR, to_curses_attr(theme.COLOR_ROSTER_ERROR)) - if contact.tune: - self.addstr(theme.CHAR_ROSTER_TUNE, - to_curses_attr(theme.COLOR_ROSTER_TUNE)) - if contact.activity: - self.addstr(theme.CHAR_ROSTER_ACTIVITY, - to_curses_attr(theme.COLOR_ROSTER_ACTIVITY)) - if contact.mood: - self.addstr(theme.CHAR_ROSTER_MOOD, - to_curses_attr(theme.COLOR_ROSTER_MOOD)) - if contact.gaming: - self.addstr(theme.CHAR_ROSTER_GAMING, - to_curses_attr(theme.COLOR_ROSTER_GAMING)) self.finish_line() def draw_resource_line(self, y: int, resource: Resource, colored: bool) -> None: @@ -394,32 +374,6 @@ class ContactInfoWin(Win): self.finish_line() i += 1 - if contact.tune: - self.addstr(i, 0, - 'Tune: %s' % common.format_tune_string(contact.tune), - to_curses_attr(theme.COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - - if contact.mood: - self.addstr(i, 0, 'Mood: %s' % contact.mood, - to_curses_attr(theme.COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - - if contact.activity: - self.addstr(i, 0, 'Activity: %s' % contact.activity, - to_curses_attr(theme.COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - - if contact.gaming: - self.addstr( - i, 0, 'Game: %s' % common.format_gaming_string(contact.gaming), - to_curses_attr(theme.COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - def draw_group_info(self, group: RosterGroup) -> None: """ draw the group information -- cgit v1.2.3