Compare commits
	
		
			1 Commits
		
	
	
		
			b9b31a494a
			...
			5e99e894e9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5e99e894e9 | 
| @ -40,7 +40,8 @@ dependencies = [ | |||||||
|     "pillow-heif", |     "pillow-heif", | ||||||
|     "IP2Location", |     "IP2Location", | ||||||
|     "bleach", |     "bleach", | ||||||
|     "sentry-sdk" |     "sentry-sdk", | ||||||
|  |     "aiosqlite" | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [tool.setuptools.packages.find] | [tool.setuptools.packages.find] | ||||||
|  | |||||||
| @ -6,11 +6,10 @@ import uuid | |||||||
| import signal | import signal | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from contextlib import asynccontextmanager | from contextlib import asynccontextmanager | ||||||
| import aiohttp_debugtoolbar |  | ||||||
| 
 | 
 | ||||||
| from snek import snode | from snek import snode | ||||||
| from snek.view.threads import ThreadsView | from snek.view.threads import ThreadsView | ||||||
| 
 | from snek.system.ads import AsyncDataSet | ||||||
| logging.basicConfig(level=logging.DEBUG) | logging.basicConfig(level=logging.DEBUG) | ||||||
| from concurrent.futures import ThreadPoolExecutor | from concurrent.futures import ThreadPoolExecutor | ||||||
| from ipaddress import ip_address | from ipaddress import ip_address | ||||||
| @ -143,6 +142,7 @@ class Application(BaseApplication): | |||||||
|             client_max_size=1024 * 1024 * 1024 * 5 * args, |             client_max_size=1024 * 1024 * 1024 * 5 * args, | ||||||
|             **kwargs, |             **kwargs, | ||||||
|         ) |         ) | ||||||
|  |         self.db = AsyncDataSet(kwargs["db_path"].replace("sqlite:///", "")) | ||||||
|         session_setup(self, EncryptedCookieStorage(SESSION_KEY)) |         session_setup(self, EncryptedCookieStorage(SESSION_KEY)) | ||||||
|         self.tasks = asyncio.Queue() |         self.tasks = asyncio.Queue() | ||||||
|         self._middlewares.append(session_middleware) |         self._middlewares.append(session_middleware) | ||||||
| @ -175,9 +175,7 @@ class Application(BaseApplication): | |||||||
|         self.on_startup.append(self.prepare_asyncio) |         self.on_startup.append(self.prepare_asyncio) | ||||||
|         self.on_startup.append(self.start_user_availability_service) |         self.on_startup.append(self.start_user_availability_service) | ||||||
|         self.on_startup.append(self.start_ssh_server) |         self.on_startup.append(self.start_ssh_server) | ||||||
|         self.on_startup.append(self.prepare_database) |         #self.on_startup.append(self.prepare_database) | ||||||
|          |  | ||||||
| 
 |  | ||||||
|          |          | ||||||
|     async def prepare_stats(self, app): |     async def prepare_stats(self, app): | ||||||
|         app['stats'] = create_stats_structure() |         app['stats'] = create_stats_structure() | ||||||
| @ -248,18 +246,8 @@ class Application(BaseApplication): | |||||||
|      |      | ||||||
| 
 | 
 | ||||||
|     async def prepare_database(self, app): |     async def prepare_database(self, app): | ||||||
|         self.db.query("PRAGMA journal_mode=WAL") |         await self.db.query_raw("PRAGMA journal_mode=WAL") | ||||||
|         self.db.query("PRAGMA syncnorm=off") |         await self.db.query_raw("PRAGMA syncnorm=off") | ||||||
| 
 |  | ||||||
|         try: |  | ||||||
|             if not self.db["user"].has_index("username"): |  | ||||||
|                 self.db["user"].create_index("username", unique=True) |  | ||||||
|             if not self.db["channel_member"].has_index(["channel_uid", "user_uid"]): |  | ||||||
|                 self.db["channel_member"].create_index(["channel_uid", "user_uid"]) |  | ||||||
|             if not self.db["channel_message"].has_index(["channel_uid", "user_uid"]): |  | ||||||
|                 self.db["channel_message"].create_index(["channel_uid", "user_uid"]) |  | ||||||
|         except: |  | ||||||
|             pass |  | ||||||
| 
 | 
 | ||||||
|         await self.services.drive.prepare_all() |         await self.services.drive.prepare_all() | ||||||
|         self.loop.create_task(self.task_runner()) |         self.loop.create_task(self.task_runner()) | ||||||
| @ -290,9 +278,9 @@ class Application(BaseApplication): | |||||||
|         self.router.add_view("/login.json", LoginView) |         self.router.add_view("/login.json", LoginView) | ||||||
|         self.router.add_view("/register.html", RegisterView) |         self.router.add_view("/register.html", RegisterView) | ||||||
|         self.router.add_view("/register.json", RegisterView) |         self.router.add_view("/register.json", RegisterView) | ||||||
|        # self.router.add_view("/drive/{rel_path:.*}", DriveView) |         self.router.add_view("/drive/{rel_path:.*}", DriveView) | ||||||
|        ## self.router.add_view("/drive.bin", UploadView) |         self.router.add_view("/drive.bin", UploadView) | ||||||
|        # self.router.add_view("/drive.bin/{uid}.{ext}", UploadView) |         self.router.add_view("/drive.bin/{uid}.{ext}", UploadView) | ||||||
|         self.router.add_view("/search-user.html", SearchUserView) |         self.router.add_view("/search-user.html", SearchUserView) | ||||||
|         self.router.add_view("/search-user.json", SearchUserView) |         self.router.add_view("/search-user.json", SearchUserView) | ||||||
|         self.router.add_view("/avatar/{uid}.svg", AvatarView) |         self.router.add_view("/avatar/{uid}.svg", AvatarView) | ||||||
| @ -300,25 +288,25 @@ class Application(BaseApplication): | |||||||
|         self.router.add_get("/http-photo", self.handle_http_photo) |         self.router.add_get("/http-photo", self.handle_http_photo) | ||||||
|         self.router.add_get("/rpc.ws", RPCView) |         self.router.add_get("/rpc.ws", RPCView) | ||||||
|         self.router.add_get("/c/{channel:.*}", ChannelView) |         self.router.add_get("/c/{channel:.*}", ChannelView) | ||||||
|         #self.router.add_view( |         self.router.add_view( | ||||||
|         #    "/channel/{channel_uid}/attachment.bin", ChannelAttachmentView |             "/channel/{channel_uid}/attachment.bin", ChannelAttachmentView | ||||||
|         #) |         ) | ||||||
|         #self.router.add_view( |         self.router.add_view( | ||||||
|         #    "/channel/{channel_uid}/drive.json", ChannelDriveApiView |             "/channel/{channel_uid}/drive.json", ChannelDriveApiView | ||||||
|         #) |         ) | ||||||
|         self.router.add_view( |         self.router.add_view( | ||||||
|             "/channel/{channel_uid}/attachment.sock", ChannelAttachmentUploadView |             "/channel/{channel_uid}/attachment.sock", ChannelAttachmentUploadView | ||||||
|         ) |         ) | ||||||
|         self.router.add_view( |         self.router.add_view( | ||||||
|             "/channel/attachment/{relative_url:.*}", ChannelAttachmentView |             "/channel/attachment/{relative_url:.*}", ChannelAttachmentView | ||||||
|         )# |         ) | ||||||
|         self.router.add_view("/channel/{channel}.html", WebView) |         self.router.add_view("/channel/{channel}.html", WebView) | ||||||
|         self.router.add_view("/threads.html", ThreadsView) |         self.router.add_view("/threads.html", ThreadsView) | ||||||
|         self.router.add_view("/terminal.ws", TerminalSocketView) |         self.router.add_view("/terminal.ws", TerminalSocketView) | ||||||
|         self.router.add_view("/terminal.html", TerminalView) |         self.router.add_view("/terminal.html", TerminalView) | ||||||
|         #self.router.add_view("/drive.json", DriveApiView) |         self.router.add_view("/drive.json", DriveApiView) | ||||||
|         #self.router.add_view("/drive.html", DriveView) |         self.router.add_view("/drive.html", DriveView) | ||||||
|         #self.router.add_view("/drive/{drive}.json", DriveView) |         self.router.add_view("/drive/{drive}.json", DriveView) | ||||||
|         self.router.add_get("/stats.html", stats_handler) |         self.router.add_get("/stats.html", stats_handler) | ||||||
|         self.router.add_view("/stats.json", StatsView) |         self.router.add_view("/stats.json", StatsView) | ||||||
|         self.router.add_view("/user/{user}.html", UserView) |         self.router.add_view("/user/{user}.html", UserView) | ||||||
| @ -499,7 +487,6 @@ class Application(BaseApplication): | |||||||
|             raise raised_exception |             raise raised_exception | ||||||
| 
 | 
 | ||||||
| app = Application(db_path="sqlite:///snek.db") | app = Application(db_path="sqlite:///snek.db") | ||||||
| #aiohttp_debugtoolbar.setup(app) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| async def main(): | async def main(): | ||||||
|  | |||||||
| @ -23,7 +23,7 @@ class ChannelModel(BaseModel): | |||||||
|             history_start_filter = f" AND created_at > '{self['history_start']}' " |             history_start_filter = f" AND created_at > '{self['history_start']}' " | ||||||
|         try: |         try: | ||||||
|             async for model in self.app.services.channel_message.query( |             async for model in self.app.services.channel_message.query( | ||||||
|                 "SELECT uid FROM channel_message WHERE channel_uid=:channel_uid" + history_start_filter + " ORDER BY id DESC LIMIT 1", |                 "SELECT uid FROM channel_message WHERE channel_uid=:channel_uid" + history_start_filter + " ORDER BY created_at DESC LIMIT 1", | ||||||
|                 {"channel_uid": self["uid"]}, |                 {"channel_uid": self["uid"]}, | ||||||
|             ): |             ): | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,11 +1,6 @@ | |||||||
| from snek.system.service import BaseService | from snek.system.service import BaseService | ||||||
| from snek.system.template import sanitize_html | from snek.system.template import sanitize_html | ||||||
| import time | import time | ||||||
| import asyncio |  | ||||||
| from concurrent.futures import ThreadPoolExecutor |  | ||||||
| 
 |  | ||||||
| executor = ThreadPoolExecutor(max_workers=50) |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class ChannelMessageService(BaseService): | class ChannelMessageService(BaseService): | ||||||
|     mapper_name = "channel_message" |     mapper_name = "channel_message" | ||||||
| @ -74,11 +69,10 @@ class ChannelMessageService(BaseService): | |||||||
|                 "color": user["color"], |                 "color": user["color"], | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|         loop = asyncio.get_event_loop() |  | ||||||
|         try: |         try: | ||||||
|             template = self.app.jinja2_env.get_template("message.html") |             template = self.app.jinja2_env.get_template("message.html") | ||||||
|             model["html"] = await loop.run_in_executor(executor, lambda: template.render(**context)) |             model["html"] = template.render(**context) | ||||||
|             model['html'] = await loop.run_in_executor(executor, lambda:sanitize_html(model['html'])) |             model['html'] = sanitize_html(model['html']) | ||||||
|         except Exception as ex: |         except Exception as ex: | ||||||
|             print(ex, flush=True) |             print(ex, flush=True) | ||||||
| 
 | 
 | ||||||
| @ -134,10 +128,8 @@ class ChannelMessageService(BaseService): | |||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|         template = self.app.jinja2_env.get_template("message.html") |         template = self.app.jinja2_env.get_template("message.html") | ||||||
|          |         model["html"] = template.render(**context) | ||||||
|         loop = asyncio.get_event_loop() |         model['html'] = sanitize_html(model['html']) | ||||||
|         model["html"] = await loop.run_in_executor(executor, lambda: template.render(**context)) |  | ||||||
|         model['html'] = await loop.run_in_executor(executor, lambda: sanitize_html(model['html'])) |  | ||||||
|         return await super().save(model) |         return await super().save(model) | ||||||
| 
 | 
 | ||||||
|     async def offset(self, channel_uid, page=0, timestamp=None, page_size=30): |     async def offset(self, channel_uid, page=0, timestamp=None, page_size=30): | ||||||
|  | |||||||
| @ -44,29 +44,19 @@ class SocketService(BaseService): | |||||||
| 
 | 
 | ||||||
|     async def user_availability_service(self): |     async def user_availability_service(self): | ||||||
|         logger.info("User availability update service started.") |         logger.info("User availability update service started.") | ||||||
|         logger.debug("Entering the main loop.") |  | ||||||
|         while True: |         while True: | ||||||
|             logger.info("Updating user availability...") |             logger.info("Updating user availability...") | ||||||
|             logger.debug("Initializing users_updated list.") |  | ||||||
|             users_updated = [] |             users_updated = [] | ||||||
|             logger.debug("Iterating over sockets.") |  | ||||||
|             for s in self.sockets: |             for s in self.sockets: | ||||||
|                 logger.debug(f"Checking connection status for socket: {s}.") |  | ||||||
|                 if not s.is_connected: |                 if not s.is_connected: | ||||||
|                     logger.debug("Socket is not connected, continuing to next socket.") |  | ||||||
|                     continue |                     continue | ||||||
|                 logger.debug(f"Checking if user {s.user} is already updated.") |  | ||||||
|                 if s.user not in users_updated: |                 if s.user not in users_updated: | ||||||
|                     logger.debug(f"Updating last_ping for user: {s.user}.") |  | ||||||
|                     s.user["last_ping"] = now() |                     s.user["last_ping"] = now() | ||||||
|                     logger.debug(f"Saving user {s.user} to the database.") |  | ||||||
|                     await self.app.services.user.save(s.user) |                     await self.app.services.user.save(s.user) | ||||||
|                     logger.debug(f"Adding user {s.user} to users_updated list.") |  | ||||||
|                     users_updated.append(s.user) |                     users_updated.append(s.user) | ||||||
|             logger.info( |             logger.info( | ||||||
|                 f"Updated user availability for {len(users_updated)} online users." |                 f"Updated user availability for {len(users_updated)} online users." | ||||||
|             ) |             ) | ||||||
|             logger.debug("Sleeping for 60 seconds before the next update.") |  | ||||||
|             await asyncio.sleep(60) |             await asyncio.sleep(60) | ||||||
| 
 | 
 | ||||||
|     async def add(self, ws, user_uid): |     async def add(self, ws, user_uid): | ||||||
|  | |||||||
| @ -101,6 +101,8 @@ class UserService(BaseService): | |||||||
|         model.username.value = username |         model.username.value = username | ||||||
|         model.password.value = await security.hash(password) |         model.password.value = await security.hash(password) | ||||||
|         if await self.save(model): |         if await self.save(model): | ||||||
|  |             for x in range(10): | ||||||
|  |                 print("Jazeker!!!") | ||||||
|             if model: |             if model: | ||||||
|                 channel = await self.services.channel.ensure_public_channel( |                 channel = await self.services.channel.ensure_public_channel( | ||||||
|                     model["uid"] |                     model["uid"] | ||||||
|  | |||||||
| @ -7,7 +7,8 @@ class UserPropertyService(BaseService): | |||||||
|     mapper_name = "user_property" |     mapper_name = "user_property" | ||||||
| 
 | 
 | ||||||
|     async def set(self, user_uid, name, value): |     async def set(self, user_uid, name, value): | ||||||
|         self.mapper.db["user_property"].upsert( |         self.mapper.db.upsert( | ||||||
|  |                 "user_property", | ||||||
|             { |             { | ||||||
|                 "user_uid": user_uid, |                 "user_uid": user_uid, | ||||||
|                 "name": name, |                 "name": name, | ||||||
|  | |||||||
							
								
								
									
										1207
									
								
								src/snek/system/ads.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1207
									
								
								src/snek/system/ads.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,7 +1,7 @@ | |||||||
| DEFAULT_LIMIT = 30 | DEFAULT_LIMIT = 30 | ||||||
| import asyncio | import asyncio | ||||||
| import typing | import typing | ||||||
| 
 | import traceback | ||||||
| from snek.system.model import BaseModel | from snek.system.model import BaseModel | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -51,7 +51,9 @@ class BaseMapper: | |||||||
|             kwargs["uid"] = uid |             kwargs["uid"] = uid | ||||||
|         if not kwargs.get("deleted_at"): |         if not kwargs.get("deleted_at"): | ||||||
|             kwargs["deleted_at"] = None |             kwargs["deleted_at"] = None | ||||||
|         record = await self.run_in_executor(self.table.find_one, **kwargs) |         #traceback.print_exc() | ||||||
|  | 
 | ||||||
|  |         record = await self.db.get(self.table_name, kwargs) | ||||||
|         if not record: |         if not record: | ||||||
|             return None |             return None | ||||||
|         record = dict(record) |         record = dict(record) | ||||||
| @ -61,23 +63,29 @@ class BaseMapper: | |||||||
|         return model |         return model | ||||||
| 
 | 
 | ||||||
|     async def exists(self, **kwargs): |     async def exists(self, **kwargs): | ||||||
|         return await self.run_in_executor(self.table.exists, **kwargs) |         return await self.db.count(self.table_name, kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         #return await self.run_in_executor(self.table.exists, **kwargs) | ||||||
| 
 | 
 | ||||||
|     async def count(self, **kwargs) -> int: |     async def count(self, **kwargs) -> int: | ||||||
|         return await self.run_in_executor(self.table.count, **kwargs) |         return await self.db.count(self.table_name,kwargs) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     async def save(self, model: BaseModel) -> bool: |     async def save(self, model: BaseModel) -> bool: | ||||||
|         if not model.record.get("uid"): |         if not model.record.get("uid"): | ||||||
|             raise Exception(f"Attempt to save without uid: {model.record}.") |             raise Exception(f"Attempt to save without uid: {model.record}.") | ||||||
|         model.updated_at.update()  |         model.updated_at.update()  | ||||||
|         return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True) |         await self.upsert(model) | ||||||
|  |         return model | ||||||
|  |         #return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True) | ||||||
| 
 | 
 | ||||||
|     async def find(self, **kwargs) -> typing.AsyncGenerator: |     async def find(self, **kwargs) -> typing.AsyncGenerator: | ||||||
|         if not kwargs.get("_limit"): |         if not kwargs.get("_limit"): | ||||||
|             kwargs["_limit"] = self.default_limit |             kwargs["_limit"] = self.default_limit | ||||||
|         if not kwargs.get("deleted_at"): |         if not kwargs.get("deleted_at"): | ||||||
|             kwargs["deleted_at"] = None |             kwargs["deleted_at"] = None | ||||||
|         for record in await self.run_in_executor(self.table.find, **kwargs): |         for record in await self.db.find(self.table_name, kwargs): | ||||||
|             model = await self.new() |             model = await self.new() | ||||||
|             for key, value in record.items(): |             for key, value in record.items(): | ||||||
|                 model[key] = value |                 model[key] = value | ||||||
| @ -88,21 +96,21 @@ class BaseMapper: | |||||||
|         return "insert" in sql or "update" in sql or "delete" in sql |         return "insert" in sql or "update" in sql or "delete" in sql | ||||||
| 
 | 
 | ||||||
|     async def query(self, sql, *args): |     async def query(self, sql, *args): | ||||||
|         for record in await self.run_in_executor(self.db.query, sql, *args, use_semaphore=await self._use_semaphore(sql)): |         for record in await self.db.query(sql, *args): | ||||||
|             yield dict(record) |             yield dict(record) | ||||||
| 
 | 
 | ||||||
|     async def update(self, model): |     async def update(self, model): | ||||||
|         if not model["deleted_at"] is None: |         if not model["deleted_at"] is None: | ||||||
|             raise Exception("Can't update deleted record.") |             raise Exception("Can't update deleted record.") | ||||||
|         model.updated_at.update() |         model.updated_at.update() | ||||||
|         return await self.run_in_executor(self.table.update, model.record, ["uid"],use_semaphore=True) |         return await self.db.update(self.table_name, model.record, {"uid": model["uid"]}) | ||||||
| 
 | 
 | ||||||
|     async def upsert(self, model): |     async def upsert(self, model): | ||||||
|         model.updated_at.update() |         model.updated_at.update() | ||||||
|         return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True) |         await self.db.upsert(self.table_name, model.record, {"uid": model["uid"]}) | ||||||
|  |         return model | ||||||
| 
 | 
 | ||||||
|     async def delete(self, **kwargs) -> int: |     async def delete(self, **kwargs) -> int: | ||||||
|         if not kwargs or not isinstance(kwargs, dict): |         if not kwargs or not isinstance(kwargs, dict): | ||||||
|             raise Exception("Can't execute delete with no filter.") |             raise Exception("Can't execute delete with no filter.") | ||||||
|         kwargs["use_semaphore"] = True |         return await self.db.delete(self.table_name, kwargs) | ||||||
|         return await self.run_in_executor(self.table.delete, **kwargs) |  | ||||||
|  | |||||||
| @ -50,7 +50,7 @@ class BaseService: | |||||||
|             if result and result.__class__ == self.mapper.model_class: |             if result and result.__class__ == self.mapper.model_class: | ||||||
|                 return result |                 return result | ||||||
|             kwargs["uid"] = uid |             kwargs["uid"] = uid | ||||||
|          |         print(kwargs,"ZZZZZZZ")  | ||||||
|         result = await self.mapper.get(**kwargs) |         result = await self.mapper.get(**kwargs) | ||||||
|         if result: |         if result: | ||||||
|             await self.cache.set(result["uid"], result) |             await self.cache.set(result["uid"], result) | ||||||
|  | |||||||
| @ -529,8 +529,8 @@ class RPCView(BaseView): | |||||||
|             try: |             try: | ||||||
|                 await self.ws.send_str(json.dumps(obj, default=str)) |                 await self.ws.send_str(json.dumps(obj, default=str)) | ||||||
|             except Exception as ex: |             except Exception as ex: | ||||||
|                 print("THIS IS THE DeAL>",str(ex), flush=True) |  | ||||||
|                 await self.services.socket.delete(self.ws) |                 await self.services.socket.delete(self.ws) | ||||||
|  |                 await self.ws.close() | ||||||
|          |          | ||||||
|         async def get_online_users(self, channel_uid): |         async def get_online_users(self, channel_uid): | ||||||
|             self._require_login() |             self._require_login() | ||||||
| @ -638,7 +638,7 @@ class RPCView(BaseView): | |||||||
|                 try: |                 try: | ||||||
|                     await rpc(msg.json()) |                     await rpc(msg.json()) | ||||||
|                 except Exception as ex: |                 except Exception as ex: | ||||||
|                     print("XXXXXXXXXX Deleting socket", ex, flush=True) |                     print("Deleting socket", ex, flush=True) | ||||||
|                     logger.exception(ex) |                     logger.exception(ex) | ||||||
|                     await self.services.socket.delete(ws) |                     await self.services.socket.delete(ws) | ||||||
|                     break |                     break | ||||||
|  | |||||||
| @ -38,6 +38,10 @@ class WebView(BaseView): | |||||||
|         channel = await self.services.channel.get( |         channel = await self.services.channel.get( | ||||||
|             uid=self.request.match_info.get("channel") |             uid=self.request.match_info.get("channel") | ||||||
|         ) |         ) | ||||||
|  |         print(self.session.get("uid"),"ZZZZZZZZZZ") | ||||||
|  |         qq = await self.services.user.get(uid=self.session.get("uid")) | ||||||
|  |          | ||||||
|  |         print("GGGGGGGGGG",qq) | ||||||
|         if not channel: |         if not channel: | ||||||
|             user = await self.services.user.get( |             user = await self.services.user.get( | ||||||
|                 uid=self.request.match_info.get("channel") |                 uid=self.request.match_info.get("channel") | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user