textchat 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.1
2
+ Name: textchat
3
+ Version: 0.0.1
4
+ Author-email: Matthew Blau <mrb1105@gmail.com>
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: textual>=0.66.0
7
+ Requires-Dist: irc>=20.4.0
8
+ Requires-Dist: textual-autocomplete>=2.1.0b0
9
+ Requires-Dist: SQLAlchemy>=2.0.30
10
+ Requires-Dist: aiosqlite>=0.20.0
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "textchat"
3
+ version = "0.0.1"
4
+ authors = [
5
+ { name = "Matthew Blau", email = "mrb1105@gmail.com" },
6
+ ]
7
+ dependencies = ["textual >=0.66.0", "irc >=20.4.0", "textual-autocomplete >=2.1.0b0","SQLAlchemy >=2.0.30", "aiosqlite >=0.20.0"]
8
+ requires-python = ">=3.8"
9
+
10
+ [build-system]
11
+ requires = ["setuptools>=61.0"]
12
+ build-backend = "setuptools.build_meta"
13
+
14
+ [project.scripts]
15
+ textchat = "textchat.chat:ircchat"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
File without changes
@@ -0,0 +1,145 @@
1
+ import asyncio
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ import sys
4
+ import threading
5
+ from textual import work
6
+ from textual.widgets import Label, TabbedContent, TabPane
7
+ from irc.client import SimpleIRCClient
8
+ from datetime import datetime
9
+ import time
10
+ import ib3.auth
11
+ import ib3.connection
12
+ import irc.bot
13
+ from ..utils.channels import load_channels
14
+ from ..db.db import add_channel_to_list
15
+
16
+ #channels = load_channels()
17
+
18
+
19
+
20
+ class IRCApp(SimpleIRCClient):
21
+ def __init__(self,app, server_list, nickname, realname, ident_password, channels=load_channels()):
22
+ super().__init__()
23
+ self.app = app
24
+ self.server_list = server_list
25
+ self.server_name = server_list[0][0]
26
+ self.server_port = server_list[0][1]
27
+ self.nickname = nickname
28
+ self.realname= realname
29
+ self.ident_password = ident_password
30
+ self.channels = channels
31
+ self._thread_pool = ThreadPoolExecutor(max_workers=1)
32
+
33
+ def start_event_loop(self):
34
+ print("connecting..")
35
+ self._thread_pool.submit(self._irc_event_loop)
36
+ print("actually connecting")
37
+
38
+ def _irc_event_loop(self):
39
+ self.start()
40
+ print("really connecting!!!!")
41
+ def stop(self):
42
+ self._thread_pool.shutdown(wait=False)
43
+
44
+
45
+ def start(self):
46
+ self.connect(
47
+ self.server_name,
48
+ self.server_port,
49
+ self.nickname,
50
+ self.ident_password,
51
+ self.realname,
52
+ )
53
+ super().start()
54
+
55
+ def on_welcome(self, connection, event):
56
+ #self.connection.privmsg("nickserv", f'identify {self.password}')
57
+ for channel in self.channels:
58
+ connection.join(channel)
59
+ #await add_channel_to_list(channel)
60
+ time.sleep(7)
61
+ self.app.notify("Connected!")
62
+
63
+
64
+ async def _intercept_join(self, message):
65
+ self.tab = self.app.query_one(TabbedContent).active_pane
66
+ if message.startswith("/join"):
67
+ channel = message.split()[1]
68
+ if not channel in self.channels:
69
+ self.connection.join(channel)
70
+ self.channels.append(channel)
71
+ await add_channel_to_list(channel)
72
+ self.app.query_one(TabbedContent).add_pane(TabPane(channel,
73
+ Label(),
74
+ name=channel,
75
+ id=f'{channel.replace("#", "").lower()}'))
76
+ else:
77
+ self.app.notify(f"Already in {channel}!")
78
+ return True
79
+ return False
80
+
81
+ def _intercept_part(self, message):
82
+ self.tab = self.app.query_one(TabbedContent).active_pane
83
+ if message.startswith("/part"):
84
+ channel = message.split()[1]
85
+ self.connection.part(channel)
86
+ self.channels.remove(channel)
87
+ self.app.query_one(TabbedContent).remove_pane(f'{channel.replace("#", "").lower()}')
88
+ return True
89
+ return False
90
+
91
+ def on_pubmsg(self, connection, event):
92
+ print(event)
93
+ sender = event.source.nick
94
+ message = event.arguments[0]
95
+ channel = event.target
96
+ now = datetime.now()
97
+ if now.minute <= 9:
98
+ self.app.handle_irc_message(f'{now.hour}:0{now.minute}', channel, sender, message, classes=None)
99
+ else:
100
+ self.app.handle_irc_message(f'{now.hour}:{now.minute}', channel, f'{sender}', message, classes=None)
101
+
102
+ def on_privmsg(self, connection, event):
103
+ message = event.arguments[0]
104
+ user = event.source.nick
105
+ now = datetime.now()
106
+ if now.minute <= 9:
107
+ self.app.handle_private_message(f'{now.hour}:0{now.minute}', user, message, classes=None)
108
+ else:
109
+ self.app.handle_private_message(f'{now.hour}:{now.minute}', user, message, classes=None)
110
+
111
+ def on_ctcp(self, connection, event):
112
+ sender = event.source.nick
113
+ message = event.arguments[1]
114
+ channel = event.target
115
+ now = datetime.now()
116
+ classes = "italics"
117
+ if now.minute <= 9:
118
+
119
+ self.app.handle_irc_message(f'{now.hour}:0{now.minute}', channel, f'{sender}', message, classes)
120
+ else:
121
+ self.app.handle_irc_message(f'{now.hour}:{now.minute}', channel, f'{sender}', message, classes)
122
+
123
+ def on_namreply(self, connection, event):
124
+ channel = event.arguments[1]
125
+ user_list = event.arguments[2].split()
126
+ self.app.add_to_tree(channel, user_list)
127
+
128
+ def on_join(self, connection, event):
129
+ sender = event.source.nick
130
+ try:
131
+ message = event.arguments[0]
132
+ except IndexError:
133
+ message = "has joined"
134
+ channel = event.target
135
+ now = datetime.now()
136
+ classes = "italics"
137
+ if now.minute <= 9 and self.nickname != sender:
138
+ self.app.handle_irc_message(f'{now.hour}:0{now.minute}', channel, f'{sender}', message, classes)
139
+ elif self.nickname != sender:
140
+ self.app.handle_irc_message(f'{now.hour}:{now.minute}', channel, f'{sender}', message, classes)
141
+
142
+ def on_disconnect(self):
143
+ self._stop_event.set()
144
+
145
+
File without changes
@@ -0,0 +1,170 @@
1
+ from datetime import datetime
2
+ from pathlib import Path
3
+ import time
4
+ try:
5
+ from .screens.quit import QuitScreen
6
+ except:
7
+ from textchat.chat.screens.quit import QuitScreen
8
+ try:
9
+ from .screens.settings import SettingsScreen
10
+ except:
11
+ from textchat.chat.screens.settings import SettingsScreen
12
+ from textchat.db.db import get_password, get_port, get_server_address, get_username
13
+ from textual import work
14
+ from textual import on
15
+ from textual.app import App
16
+ from textual.command import Command
17
+ from textual.widgets import Input, Label, TabbedContent, TabPane,Tree
18
+ from textual.worker import get_current_worker
19
+ try:
20
+ from .client import IRCApp
21
+ except:
22
+ from textchat.chat.client import IRCApp
23
+ try:
24
+ from .screens.irc import IRCScreen
25
+ except:
26
+ from textchat.chat.screens.irc import IRCScreen
27
+ try:
28
+ from .screens.settings import SettingsScreen
29
+ except:
30
+ from textchat.chat.screens.settings import SettingsScreen
31
+ try:
32
+ from ..db.base import create_table
33
+ except:
34
+ from textchat.db.base import create_table
35
+
36
+ try:
37
+ from ..utils.channels import load_channels
38
+ except:
39
+ from textchat.utils.channels import load_channels
40
+
41
+
42
+ class TextChat(App):
43
+ def __init__(self):
44
+ super().__init__()
45
+ CSS_PATH = 'irc.tcss'
46
+ SCREENS = {"irc": IRCScreen(), "settings":SettingsScreen() }
47
+ BINDINGS = [("ctrl+h", "return_home", "Home"),
48
+ ("ctrl+s", "open_settings", "Settings"),
49
+ ("ctrl+q", "request_quit", "Quit"),
50
+ ("ctrl+d", "toggle_dark", "Toggle dark mode")
51
+ ]
52
+
53
+
54
+
55
+ async def on_mount(self) -> None:
56
+ self.channel_list = None
57
+ self.users = set()
58
+ self.username = await get_username()
59
+ self.port = await get_port()
60
+ self.server_address = await get_server_address()
61
+ self.password = await get_password()
62
+ await create_table()
63
+
64
+ existing_channels = await load_channels()
65
+ if not existing_channels:
66
+ self.app.push_screen(self.SCREENS['settings'])
67
+ else:
68
+ self.app.push_screen(self.SCREENS['irc'])
69
+ self.irc_client = IRCApp(self,
70
+ server_list=[(self.server_address, self.port)],
71
+ nickname=self.username,
72
+ realname=self.username,
73
+ ident_password=self.password,
74
+ channels=existing_channels,
75
+ )
76
+ self.irc_client.start_event_loop()
77
+
78
+
79
+
80
+ def action_toggle_dark(self) -> None:
81
+ """An action to toggle dark mode."""
82
+ self.dark = not self.dark
83
+
84
+ def action_request_quit(self) -> None:
85
+
86
+ def check_quit(quit: bool) -> None:
87
+ if quit:
88
+ self.app.exit()
89
+ self.irc_client.stop()
90
+
91
+ self.push_screen(QuitScreen(), check_quit)
92
+
93
+ def action_open_settings(self) -> None:
94
+ self.push_screen(SettingsScreen())
95
+
96
+ def action_return_home(self) -> None:
97
+ self.push_screen(self.SCREENS['irc'])
98
+
99
+ @on(Input.Submitted)
100
+ async def send_message(self, event) -> None:
101
+ self.query_one(Input).value = ""
102
+ self.tab = self.query_one(TabbedContent).active_pane
103
+ now = datetime.now()
104
+ if now.minute <= 9 and not await self.irc_client._intercept_join(event.value):
105
+ self.irc_client.connection.privmsg(self.tab.name, event.value)
106
+ self.tab.mount(Label(f'{now.hour}:0{now.minute} <{self.irc_client.nickname}> {event.value}'))
107
+ elif not await self.irc_client._intercept_join(event.value):
108
+ self.irc_client.connection.privmsg(self.tab.name, event.value)
109
+ self.tab.mount(Label(f'{now.hour}:{now.minute} <{self.irc_client.nickname}> {event.value}'))
110
+
111
+ def get_channel_list(self):
112
+ if self.app.channel_list is None:
113
+ return None
114
+ else:
115
+ return self.app.channel_list
116
+
117
+ def irc_message(self, time, channel, sender, message,classes):
118
+ self.tab = self.app.query_one(TabbedContent).get_pane(channel.replace("#", "").lower())
119
+ if self.irc_client.nickname in message:
120
+ self.app.notify(f"{time} <{sender}> {message}", title=channel)
121
+ self.tab.mount(Label(f"{time} <{sender}> {message}", classes="highlight"))
122
+ else:
123
+ self.tab.mount(Label(f'{time} <{sender}> {message}', classes=classes))
124
+
125
+
126
+ def received_private_message(self, time, sender, message, classes):
127
+ self.tab = self.app.query_one(TabbedContent).add_pane(TabPane(sender,
128
+ Label(),
129
+ name=sender,
130
+ id=f'{sender}'))
131
+ self.tab = self.app.query_one(TabbedContent).get_pane(f'{sender}')
132
+ self.tab.mount(Label(f'{time} <{sender}> {message}', classes=classes))
133
+
134
+
135
+ def add_to_tree(self, channel, user_list):
136
+ tree = self.app.query_one(Tree)
137
+ self.app.channel_list = self.app.get_channel_list()
138
+ if self.app.channel_list is not None and channel != self.app.channel_list.data:
139
+ channels_list = tree.root.add(channel, data=channel)
140
+ self.app.channel_list = channels_list
141
+ elif self.channel_list is None:
142
+ channels_list = tree.root.add(channel, data=channel)
143
+ self.app.channel_list = channels_list
144
+ for user in user_list:
145
+ self.app.channel_list.add_leaf(user)
146
+ for user in user_list:
147
+ self.app.users.add(user)
148
+ else:
149
+ pass
150
+
151
+ @work(exclusive=True)
152
+ async def handle_irc_message(self, time, channel, sender, message, classes):
153
+ worker = get_current_worker()
154
+ if not worker.is_cancelled:
155
+ self.irc_message(time, channel, sender, message, classes)
156
+ else:
157
+ pass
158
+
159
+ @work(exclusive=True)
160
+ async def handle_private_message(self, time, sender, message, classes):
161
+ worker = get_current_worker()
162
+ if not worker.is_cancelled:
163
+ self.received_private_message(time, sender, message, classes)
164
+ else:
165
+ pass
166
+ async def on_shutdown(self):
167
+ self.irc_client.on_disconnect()
168
+
169
+ app = TextChat()
170
+ app.run()
File without changes
@@ -0,0 +1,39 @@
1
+ import asyncio
2
+ from textchat.utils.channels import load_channels
3
+ from textual.app import ComposeResult
4
+ from textual.widgets import Input, Tree, Footer, TabbedContent, TabPane, Label
5
+ from textual.screen import Screen
6
+ from textual_autocomplete import DropdownItem, InputState
7
+
8
+
9
+ class IRCScreen(Screen):
10
+ try:
11
+ channels = asyncio.run(load_channels())
12
+ except:
13
+ channels = False
14
+ def compose(self) -> ComposeResult:
15
+ def get_items(input_state: InputState):
16
+ items = []
17
+ for user in self.app.users:
18
+ items.append(
19
+ DropdownItem(user.replace("+", "").replace("@", ""))
20
+ )
21
+ matches = [c for c in items if input_state.value.lower() in c.main.plain.lower()]
22
+ ordered = sorted(matches, key=lambda v: input_state.value.lower() in v.main.plain.lower(), reverse=True)
23
+ return ordered
24
+
25
+
26
+ with TabbedContent() as self.tabbed_content:
27
+ if not self.channels:
28
+ with TabPane("uh oh!"):
29
+ yield(Label("Please visit the settings tab and restart when finished to see your channels here!"))
30
+ else:
31
+ for name in self.channels:
32
+ tab_id = f'{name.replace("#", "")}'
33
+ with TabPane(name, name=name, id=tab_id.lower()):
34
+ yield(Label())
35
+ tree: Tree[dict] = Tree("Channels", id="sidebar")
36
+ yield tree
37
+ yield Input(id="chat-input")
38
+ #yield AutoComplete(Input(placeholder=f"Enter your message", id="chat-input"), Dropdown(items=get_items))
39
+ yield Footer()
@@ -0,0 +1,22 @@
1
+ from textual.app import ComposeResult
2
+ from textual.containers import Grid
3
+ from textual.screen import ModalScreen
4
+ from textual.widgets import Button, Label
5
+
6
+ class QuitScreen(ModalScreen[bool]):
7
+ """Screen with a dialog to quit."""
8
+
9
+ def compose(self) -> ComposeResult:
10
+ yield Grid(
11
+ Label("Are you sure you want to quit?", id="question"),
12
+ Button("Quit", variant="error", id="quit"),
13
+ Button("Cancel", variant="primary", id="cancel"),
14
+ id="dialog",
15
+ )
16
+
17
+ def on_button_pressed(self, event: Button.Pressed) -> None:
18
+ if event.button.id == "quit":
19
+ self.dismiss(True)
20
+
21
+ else:
22
+ self.dismiss(False)
@@ -0,0 +1,52 @@
1
+ from textchat.chat.client import IRCApp
2
+ from textchat.db.base import create_table
3
+ from textual.app import ComposeResult
4
+ from textual.screen import Screen
5
+ from textual.widgets import Input, Footer, Button
6
+ from textual.containers import Vertical
7
+ from ..widgets.address import ServerAddress, ServerPort
8
+ from ..widgets.nickname import Nickname
9
+ from ..widgets.channels import Channels
10
+ from ..widgets.password import Password
11
+ from ...db.db import add_channel_to_list, add_server_info
12
+ from ...utils.channels import load_channels
13
+
14
+ class SettingsScreen(Screen):
15
+
16
+ def compose(self) -> ComposeResult:
17
+ yield Vertical(
18
+ ServerAddress(placeholder="Server address"),
19
+ ServerPort(placeholder="port", type="integer"),
20
+ Nickname(placeholder="nickname"),
21
+ Channels(placeholder="channels to join (seperate by comma)"),
22
+ Password(placeholder="password", password=True),
23
+ Button(label="Save", id="save"),
24
+ classes="column"
25
+ )
26
+ yield Footer()
27
+ async def on_mount(self):
28
+ await create_table()
29
+
30
+
31
+ async def on_button_pressed(self, event: Button.Pressed):
32
+ if event.button.id =="save":
33
+ server_address = self.query_one(ServerAddress).value
34
+ server_port = self.query_one(ServerPort).value
35
+ nickname = self.query_one(Nickname).value
36
+ channels = self.query_one(Channels).value
37
+ password = self.query_one(Password).value
38
+ await add_server_info(server_address, server_port, nickname, password)
39
+ existing_channnels = await load_channels()
40
+ for channel in channels.split(","):
41
+ if channel not in existing_channnels:
42
+ await add_channel_to_list(channel)
43
+ else:
44
+ pass
45
+ self.irc_client = IRCApp(self,
46
+ server_list=[(server_address, server_port)],
47
+ nickname=nickname,
48
+ realname=nickname,
49
+ ident_password=password,
50
+ channels=channels,
51
+ )
52
+ self.irc_client.start_event_loop()
File without changes
@@ -0,0 +1,7 @@
1
+ from textual.widgets import Input
2
+
3
+ class ServerAddress(Input):
4
+ pass
5
+
6
+ class ServerPort(Input):
7
+ pass
@@ -0,0 +1,4 @@
1
+ from textual.widgets import Input
2
+
3
+ class Channels(Input):
4
+ pass
@@ -0,0 +1,4 @@
1
+ from textual.widgets import Input
2
+
3
+ class Nickname(Input):
4
+ pass
@@ -0,0 +1,4 @@
1
+ from textual.widgets import Input
2
+
3
+ class Password(Input):
4
+ pass
@@ -0,0 +1,47 @@
1
+ from sqlalchemy import Column, Integer, String
2
+ from sqlalchemy.ext.asyncio import create_async_engine
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+ from sqlalchemy.ext.asyncio import async_sessionmaker
5
+ from sqlalchemy.sql.schema import MetaData
6
+ import asyncio
7
+ engine = create_async_engine(
8
+ "sqlite+aiosqlite:///textchat.db", echo=False,)
9
+ Session = async_sessionmaker(
10
+ bind=engine, expire_on_commit=False)
11
+ metadata = MetaData()
12
+ from sqlalchemy.orm import DeclarativeBase
13
+
14
+ class Base(DeclarativeBase):
15
+ pass
16
+
17
+
18
+ class Channels(Base):
19
+ __tablename__ = 'channels'
20
+
21
+ id = Column(Integer, primary_key=True,
22
+ autoincrement=True)
23
+ channel_name = Column(String, unique=True)
24
+
25
+
26
+ def __init__(self, channel_name):
27
+ self.channel_name = channel_name
28
+
29
+ class ServerInfo(Base):
30
+ __tablename__ = 'server'
31
+ id = Column(Integer, primary_key=True, autoincrement=True)
32
+ server_address = Column(String, unique=True)
33
+ port = Column(Integer, unique=False)
34
+ nickname = Column(String, unique=False)
35
+ password = Column(String, unique=False)
36
+
37
+ def __init__(self, server_address, port, nickname, password):
38
+ self.server_address = server_address
39
+ self.port = port
40
+ self.nickname = nickname
41
+ self.password = password
42
+
43
+
44
+ async def create_table():
45
+ async with engine.begin() as conn:
46
+ await conn.run_sync(Base.metadata.create_all)
47
+
@@ -0,0 +1,47 @@
1
+ from sqlalchemy import select
2
+ from .base import Session, Channels, ServerInfo
3
+
4
+ async def get_channels():
5
+ async with Session() as session:
6
+ channels = await session.execute(select(Channels))
7
+ await session.commit()
8
+ channel_names = channels.scalars().all()
9
+ return channel_names
10
+
11
+ async def add_channel_to_list(channel):
12
+ async with Session() as session:
13
+ channel_name = Channels(channel_name=channel)
14
+ session.add(channel_name)
15
+ await session.commit()
16
+ return channel_name
17
+
18
+ async def add_server_info(server_address, port, nickname, password):
19
+ async with Session() as session:
20
+ server_info = ServerInfo(server_address, port, nickname, password)
21
+ session.add(server_info)
22
+ await session.commit()
23
+ return server_info
24
+
25
+ async def get_username():
26
+ async with Session() as session:
27
+ username = await session.execute(select(ServerInfo.nickname))
28
+ await session.commit()
29
+ return username.scalars().first()
30
+
31
+ async def get_password():
32
+ async with Session() as session:
33
+ password = await session.execute(select(ServerInfo.password))
34
+ await session.commit()
35
+ return password.scalars().first()
36
+
37
+ async def get_port():
38
+ async with Session() as session:
39
+ port = await session.execute(select(ServerInfo.port))
40
+ await session.commit()
41
+ return port.scalars().first()
42
+
43
+ async def get_server_address():
44
+ async with Session() as session:
45
+ server_address = await session.execute(select(ServerInfo.server_address))
46
+ await session.commit()
47
+ return server_address.scalars().first()
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,5 @@
1
+ from ..db.db import get_channels
2
+ async def load_channels():
3
+ channels = await get_channels()
4
+
5
+ return [name.channel_name for name in channels]
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.1
2
+ Name: textchat
3
+ Version: 0.0.1
4
+ Author-email: Matthew Blau <mrb1105@gmail.com>
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: textual>=0.66.0
7
+ Requires-Dist: irc>=20.4.0
8
+ Requires-Dist: textual-autocomplete>=2.1.0b0
9
+ Requires-Dist: SQLAlchemy>=2.0.30
10
+ Requires-Dist: aiosqlite>=0.20.0
@@ -0,0 +1,25 @@
1
+ pyproject.toml
2
+ src/__init__.py
3
+ src/textchat.egg-info/PKG-INFO
4
+ src/textchat.egg-info/SOURCES.txt
5
+ src/textchat.egg-info/dependency_links.txt
6
+ src/textchat.egg-info/entry_points.txt
7
+ src/textchat.egg-info/requires.txt
8
+ src/textchat.egg-info/top_level.txt
9
+ src/textchat/chat/__init__.py
10
+ src/textchat/chat/client.py
11
+ src/textchat/chat/ircchat.py
12
+ src/textchat/chat/commands/__init__.py
13
+ src/textchat/chat/screens/__init__.py
14
+ src/textchat/chat/screens/irc.py
15
+ src/textchat/chat/screens/quit.py
16
+ src/textchat/chat/screens/settings.py
17
+ src/textchat/chat/widgets/__init__.py
18
+ src/textchat/chat/widgets/address.py
19
+ src/textchat/chat/widgets/channels.py
20
+ src/textchat/chat/widgets/nickname.py
21
+ src/textchat/chat/widgets/password.py
22
+ src/textchat/db/base.py
23
+ src/textchat/db/db.py
24
+ src/textchat/db/model.py
25
+ src/textchat/utils/channels.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ textchat = textchat.chat:ircchat
@@ -0,0 +1,5 @@
1
+ textual>=0.66.0
2
+ irc>=20.4.0
3
+ textual-autocomplete>=2.1.0b0
4
+ SQLAlchemy>=2.0.30
5
+ aiosqlite>=0.20.0
@@ -0,0 +1,2 @@
1
+ __init__
2
+ textchat