DbNull の修正と型定義整備
このコミットが含まれているのは:
+121
-41
@@ -10,15 +10,20 @@ import random
|
|||||||
import string
|
import string
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import Any, TypedDict, cast
|
from typing import Any, Self, Type, TypedDict, cast
|
||||||
|
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
import requests
|
import requests
|
||||||
|
from mysql.connector.connection import MySQLConnectionAbstract
|
||||||
|
|
||||||
# TODO: “何もしなぃ” を意味する None と区別可能にするため,NULL クラスを別途用意すること
|
|
||||||
DbNull = None
|
class DbNull:
|
||||||
DbNullType = type (None)
|
def __new__ (
|
||||||
|
cls,
|
||||||
|
):
|
||||||
|
delattr (cls, '__init__')
|
||||||
|
DbNullType = Type[DbNull]
|
||||||
|
|
||||||
|
|
||||||
class VideoSearchParam (TypedDict):
|
class VideoSearchParam (TypedDict):
|
||||||
@@ -55,11 +60,58 @@ class CommentResult (TypedDict):
|
|||||||
isMyPost: bool
|
isMyPost: bool
|
||||||
|
|
||||||
|
|
||||||
|
class CommentRow (TypedDict):
|
||||||
|
id: int
|
||||||
|
video_id: int
|
||||||
|
comment_no: int
|
||||||
|
user_id: int
|
||||||
|
content: str
|
||||||
|
posted_at: datetime
|
||||||
|
nico_count: int
|
||||||
|
vpos_ms: int | None
|
||||||
|
|
||||||
|
|
||||||
|
class TagRow (TypedDict):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserRow (TypedDict):
|
||||||
|
id: int
|
||||||
|
code: str
|
||||||
|
|
||||||
|
|
||||||
|
class VideoRow (TypedDict):
|
||||||
|
id: int
|
||||||
|
code: str
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
uploaded_at: datetime
|
||||||
|
deleted_at: datetime | None
|
||||||
|
|
||||||
|
|
||||||
|
class VideoHistoryRow (TypedDict):
|
||||||
|
id: int
|
||||||
|
video_id: int
|
||||||
|
fetched_at: date
|
||||||
|
views_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class VideoTagRow (TypedDict):
|
||||||
|
id: int
|
||||||
|
video_id: int
|
||||||
|
tag_id: int
|
||||||
|
tagged_at: date
|
||||||
|
untagged_at: date | None
|
||||||
|
|
||||||
|
|
||||||
def main (
|
def main (
|
||||||
) -> None:
|
) -> None:
|
||||||
conn = mysql.connector.connect (user = os.environ['MYSQL_USER'],
|
conn = mysql.connector.connect (user = os.environ['MYSQL_USER'],
|
||||||
password = os.environ['MYSQL_PASS'],
|
password = os.environ['MYSQL_PASS'],
|
||||||
database = 'nizika_nico')
|
database = 'nizika_nico')
|
||||||
|
if not isinstance (conn, MySQLConnectionAbstract):
|
||||||
|
raise TypeError
|
||||||
|
|
||||||
now = datetime.now ()
|
now = datetime.now ()
|
||||||
|
|
||||||
@@ -248,7 +300,7 @@ def search_nico_by_tags (
|
|||||||
class VideoDao:
|
class VideoDao:
|
||||||
def __init__ (
|
def __init__ (
|
||||||
self,
|
self,
|
||||||
conn
|
conn: MySQLConnectionAbstract,
|
||||||
):
|
):
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
|
|
||||||
@@ -272,7 +324,7 @@ class VideoDao:
|
|||||||
id = %s
|
id = %s
|
||||||
ORDER BY
|
ORDER BY
|
||||||
id""", (video_id,))
|
id""", (video_id,))
|
||||||
row = c.fetchone ()
|
row = cast (VideoRow | None, c.fetchone ())
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
return self._create_dto_from_row (row, with_relation_tables)
|
return self._create_dto_from_row (row, with_relation_tables)
|
||||||
@@ -295,7 +347,7 @@ class VideoDao:
|
|||||||
ORDER BY
|
ORDER BY
|
||||||
id""")
|
id""")
|
||||||
videos: list[VideoDto] = []
|
videos: list[VideoDto] = []
|
||||||
for row in c.fetchall ():
|
for row in cast (list[VideoRow], c.fetchall ()):
|
||||||
videos.append (self._create_dto_from_row (row, with_relation_tables))
|
videos.append (self._create_dto_from_row (row, with_relation_tables))
|
||||||
return videos
|
return videos
|
||||||
|
|
||||||
@@ -316,7 +368,7 @@ class VideoDao:
|
|||||||
WHERE
|
WHERE
|
||||||
deleted_at IS NULL""")
|
deleted_at IS NULL""")
|
||||||
videos: list[VideoDto] = []
|
videos: list[VideoDto] = []
|
||||||
for row in c.fetchall ():
|
for row in cast (list[VideoRow], c.fetchall ()):
|
||||||
videos.append (self._create_dto_from_row (row, False))
|
videos.append (self._create_dto_from_row (row, False))
|
||||||
return videos
|
return videos
|
||||||
|
|
||||||
@@ -325,6 +377,13 @@ class VideoDao:
|
|||||||
video: VideoDto,
|
video: VideoDto,
|
||||||
with_relation_tables: bool,
|
with_relation_tables: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
deleted_at: datetime | DbNullType | None = video.deleted_at
|
||||||
|
if deleted_at is None:
|
||||||
|
raise TypeError ('未実装')
|
||||||
|
if deleted_at is DbNull:
|
||||||
|
deleted_at = None
|
||||||
|
deleted_at = cast (datetime | None, deleted_at)
|
||||||
|
|
||||||
with self.conn.cursor (dictionary = True) as c:
|
with self.conn.cursor (dictionary = True) as c:
|
||||||
c.execute ("""
|
c.execute ("""
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
@@ -350,7 +409,7 @@ class VideoDao:
|
|||||||
video.title,
|
video.title,
|
||||||
video.description,
|
video.description,
|
||||||
video.uploaded_at,
|
video.uploaded_at,
|
||||||
video.deleted_at))
|
deleted_at))
|
||||||
video.id_ = c.lastrowid
|
video.id_ = c.lastrowid
|
||||||
if with_relation_tables:
|
if with_relation_tables:
|
||||||
if video.video_tags is not None:
|
if video.video_tags is not None:
|
||||||
@@ -386,7 +445,7 @@ class VideoDao:
|
|||||||
|
|
||||||
def _create_dto_from_row (
|
def _create_dto_from_row (
|
||||||
self,
|
self,
|
||||||
row,
|
row: VideoRow,
|
||||||
with_relation_tables: bool,
|
with_relation_tables: bool,
|
||||||
) -> VideoDto:
|
) -> VideoDto:
|
||||||
video = VideoDto (id_ = row['id'],
|
video = VideoDto (id_ = row['id'],
|
||||||
@@ -394,7 +453,7 @@ class VideoDao:
|
|||||||
title = row['title'],
|
title = row['title'],
|
||||||
description = row['description'],
|
description = row['description'],
|
||||||
uploaded_at = row['uploaded_at'],
|
uploaded_at = row['uploaded_at'],
|
||||||
deleted_at = row['deleted_at'])
|
deleted_at = row['deleted_at'] or DbNull)
|
||||||
if with_relation_tables and video.id_ is not None:
|
if with_relation_tables and video.id_ is not None:
|
||||||
video.video_tags = VideoTagDao (self.conn).fetch_by_video_id (video.id_, False)
|
video.video_tags = VideoTagDao (self.conn).fetch_by_video_id (video.id_, False)
|
||||||
for i in range (len (video.video_tags)):
|
for i in range (len (video.video_tags)):
|
||||||
@@ -424,7 +483,7 @@ class VideoDto:
|
|||||||
class VideoTagDao:
|
class VideoTagDao:
|
||||||
def __init__ (
|
def __init__ (
|
||||||
self,
|
self,
|
||||||
conn,
|
conn: MySQLConnectionAbstract,
|
||||||
):
|
):
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
|
|
||||||
@@ -448,7 +507,7 @@ class VideoTagDao:
|
|||||||
ORDER BY
|
ORDER BY
|
||||||
id""", (video_id,))
|
id""", (video_id,))
|
||||||
video_tags: list[VideoTagDto] = []
|
video_tags: list[VideoTagDto] = []
|
||||||
for row in c.fetchall ():
|
for row in cast (list[VideoTagRow], c.fetchall ()):
|
||||||
video_tags.append (self._create_dto_from_row (row, with_relation_tables))
|
video_tags.append (self._create_dto_from_row (row, with_relation_tables))
|
||||||
return video_tags
|
return video_tags
|
||||||
|
|
||||||
@@ -473,7 +532,7 @@ class VideoTagDao:
|
|||||||
ORDER BY
|
ORDER BY
|
||||||
id""", (video_id,))
|
id""", (video_id,))
|
||||||
video_tags: list[VideoTagDto] = []
|
video_tags: list[VideoTagDto] = []
|
||||||
for row in c.fetchall ():
|
for row in cast (list[VideoTagRow], c.fetchall ()):
|
||||||
video_tags.append (self._create_dto_from_row (row, with_relation_tables))
|
video_tags.append (self._create_dto_from_row (row, with_relation_tables))
|
||||||
return video_tags
|
return video_tags
|
||||||
|
|
||||||
@@ -496,7 +555,7 @@ class VideoTagDao:
|
|||||||
WHERE
|
WHERE
|
||||||
video_id = %s
|
video_id = %s
|
||||||
AND tag_id = %s""", (video_id, tag_id))
|
AND tag_id = %s""", (video_id, tag_id))
|
||||||
row = c.fetchone ()
|
row = cast (VideoTagRow, c.fetchone ())
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
return self._create_dto_from_row (row, with_relation_tables)
|
return self._create_dto_from_row (row, with_relation_tables)
|
||||||
@@ -506,6 +565,13 @@ class VideoTagDao:
|
|||||||
video_tag: VideoTagDto,
|
video_tag: VideoTagDto,
|
||||||
with_relation_tables: bool,
|
with_relation_tables: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
untagged_at: date | DbNullType | None = video_tag.untagged_at
|
||||||
|
if untagged_at is None:
|
||||||
|
raise TypeError ('未実装')
|
||||||
|
if untagged_at is DbNull:
|
||||||
|
untagged_at = None
|
||||||
|
untagged_at = cast (date | None, untagged_at)
|
||||||
|
|
||||||
with self.conn.cursor (dictionary = True) as c:
|
with self.conn.cursor (dictionary = True) as c:
|
||||||
c.execute ("""
|
c.execute ("""
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
@@ -520,7 +586,7 @@ class VideoTagDao:
|
|||||||
%s,
|
%s,
|
||||||
%s,
|
%s,
|
||||||
%s)""", (video_tag.video_id, video_tag.tag_id,
|
%s)""", (video_tag.video_id, video_tag.tag_id,
|
||||||
video_tag.tagged_at, video_tag.untagged_at))
|
video_tag.tagged_at, untagged_at))
|
||||||
video_tag.id_ = c.lastrowid
|
video_tag.id_ = c.lastrowid
|
||||||
if with_relation_tables:
|
if with_relation_tables:
|
||||||
if video_tag.video is not None:
|
if video_tag.video is not None:
|
||||||
@@ -533,6 +599,13 @@ class VideoTagDao:
|
|||||||
video_tag: VideoTagDto,
|
video_tag: VideoTagDto,
|
||||||
with_relation_tables: bool,
|
with_relation_tables: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
untagged_at: date | DbNullType | None = video_tag.untagged_at
|
||||||
|
if untagged_at is None:
|
||||||
|
raise TypeError ('未実装')
|
||||||
|
if untagged_at is DbNull:
|
||||||
|
untagged_at = None
|
||||||
|
untagged_at = cast (date | None, untagged_at)
|
||||||
|
|
||||||
with self.conn.cursor (dictionary = True) as c:
|
with self.conn.cursor (dictionary = True) as c:
|
||||||
c.execute ("""
|
c.execute ("""
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
@@ -554,7 +627,7 @@ class VideoTagDao:
|
|||||||
untagged_at = VALUES(untagged_at)""", (video_tag.video_id,
|
untagged_at = VALUES(untagged_at)""", (video_tag.video_id,
|
||||||
video_tag.tag_id,
|
video_tag.tag_id,
|
||||||
video_tag.tagged_at,
|
video_tag.tagged_at,
|
||||||
video_tag.untagged_at))
|
untagged_at))
|
||||||
video_tag.id_ = c.lastrowid
|
video_tag.id_ = c.lastrowid
|
||||||
if with_relation_tables:
|
if with_relation_tables:
|
||||||
if video_tag.video is not None:
|
if video_tag.video is not None:
|
||||||
@@ -590,14 +663,14 @@ class VideoTagDao:
|
|||||||
|
|
||||||
def _create_dto_from_row (
|
def _create_dto_from_row (
|
||||||
self,
|
self,
|
||||||
row,
|
row: VideoTagRow,
|
||||||
with_relation_tables: bool,
|
with_relation_tables: bool,
|
||||||
) -> VideoTagDto:
|
) -> VideoTagDto:
|
||||||
video_tag = VideoTagDto (id_ = row['id'],
|
video_tag = VideoTagDto (id_ = row['id'],
|
||||||
video_id = row['video_id'],
|
video_id = row['video_id'],
|
||||||
tag_id = row['tag_id'],
|
tag_id = row['tag_id'],
|
||||||
tagged_at = row['tagged_at'],
|
tagged_at = row['tagged_at'],
|
||||||
untagged_at = row['untagged_at'])
|
untagged_at = row['untagged_at'] or DbNull)
|
||||||
if with_relation_tables:
|
if with_relation_tables:
|
||||||
video_tag.video = VideoDao (self.conn).find (video_tag.video_id, True)
|
video_tag.video = VideoDao (self.conn).find (video_tag.video_id, True)
|
||||||
video_tag.tag = TagDao (self.conn).find (video_tag.tag_id)
|
video_tag.tag = TagDao (self.conn).find (video_tag.tag_id)
|
||||||
@@ -608,17 +681,17 @@ class VideoTagDao:
|
|||||||
class VideoTagDto:
|
class VideoTagDto:
|
||||||
video_id: int
|
video_id: int
|
||||||
tag_id: int
|
tag_id: int
|
||||||
tagged_at: datetime
|
tagged_at: date
|
||||||
id_: int | None = None
|
id_: int | None = None
|
||||||
untagged_at: datetime | DbNullType = DbNull
|
untagged_at: date | DbNullType = DbNull
|
||||||
video: VideoDto | None = None
|
video: VideoDto | None = None
|
||||||
tag: TagDto | None = None
|
tag: TagDto | None = None
|
||||||
|
|
||||||
|
|
||||||
class TagDao:
|
class TagDao:
|
||||||
def __init__ (
|
def __init__ (
|
||||||
self,
|
self,
|
||||||
conn,
|
conn: MySQLConnectionAbstract,
|
||||||
):
|
):
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
|
|
||||||
@@ -635,7 +708,7 @@ class TagDao:
|
|||||||
tags
|
tags
|
||||||
WHERE
|
WHERE
|
||||||
id = %s""", (tag_id,))
|
id = %s""", (tag_id,))
|
||||||
row = c.fetchone ()
|
row = cast (TagRow | None, c.fetchone ())
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
return self._create_dto_from_row (row)
|
return self._create_dto_from_row (row)
|
||||||
@@ -653,7 +726,7 @@ class TagDao:
|
|||||||
tags
|
tags
|
||||||
WHERE
|
WHERE
|
||||||
name = %s""", (tag_name,))
|
name = %s""", (tag_name,))
|
||||||
row = c.fetchone ()
|
row = cast (TagRow | None, c.fetchone ())
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
return self._create_dto_from_row (row)
|
return self._create_dto_from_row (row)
|
||||||
@@ -686,7 +759,7 @@ class TagDao:
|
|||||||
|
|
||||||
def _create_dto_from_row (
|
def _create_dto_from_row (
|
||||||
self,
|
self,
|
||||||
row,
|
row: TagRow,
|
||||||
) -> TagDto:
|
) -> TagDto:
|
||||||
return TagDto (id_ = row['id'],
|
return TagDto (id_ = row['id'],
|
||||||
name = row['name'])
|
name = row['name'])
|
||||||
@@ -701,7 +774,7 @@ class TagDto:
|
|||||||
class VideoHistoryDao:
|
class VideoHistoryDao:
|
||||||
def __init__ (
|
def __init__ (
|
||||||
self,
|
self,
|
||||||
conn,
|
conn: MySQLConnectionAbstract,
|
||||||
):
|
):
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
|
|
||||||
@@ -722,7 +795,7 @@ class VideoHistoryDao:
|
|||||||
WHERE
|
WHERE
|
||||||
video_id = %s""", (video_id,))
|
video_id = %s""", (video_id,))
|
||||||
video_histories: list[VideoHistoryDto] = []
|
video_histories: list[VideoHistoryDto] = []
|
||||||
for row in c.fetchall ():
|
for row in cast (list[VideoHistoryRow], c.fetchall ()):
|
||||||
video_histories.append (self._create_dto_from_row (row, with_relation_tables))
|
video_histories.append (self._create_dto_from_row (row, with_relation_tables))
|
||||||
return video_histories
|
return video_histories
|
||||||
|
|
||||||
@@ -777,7 +850,7 @@ class VideoHistoryDao:
|
|||||||
|
|
||||||
def _create_dto_from_row (
|
def _create_dto_from_row (
|
||||||
self,
|
self,
|
||||||
row,
|
row: VideoHistoryRow,
|
||||||
with_relation_tables: bool,
|
with_relation_tables: bool,
|
||||||
) -> VideoHistoryDto:
|
) -> VideoHistoryDto:
|
||||||
video_history = VideoHistoryDto (id_ = row['id'],
|
video_history = VideoHistoryDto (id_ = row['id'],
|
||||||
@@ -792,7 +865,7 @@ class VideoHistoryDao:
|
|||||||
@dataclass (slots = True)
|
@dataclass (slots = True)
|
||||||
class VideoHistoryDto:
|
class VideoHistoryDto:
|
||||||
video_id: int
|
video_id: int
|
||||||
fetched_at: datetime
|
fetched_at: date
|
||||||
views_count: int
|
views_count: int
|
||||||
id_: int | None = None
|
id_: int | None = None
|
||||||
video: VideoDto | None = None
|
video: VideoDto | None = None
|
||||||
@@ -801,7 +874,7 @@ class VideoHistoryDto:
|
|||||||
class CommentDao:
|
class CommentDao:
|
||||||
def __init__ (
|
def __init__ (
|
||||||
self,
|
self,
|
||||||
conn,
|
conn: MySQLConnectionAbstract,
|
||||||
):
|
):
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
|
|
||||||
@@ -826,7 +899,7 @@ class CommentDao:
|
|||||||
WHERE
|
WHERE
|
||||||
video_id = %s""", (video_id,))
|
video_id = %s""", (video_id,))
|
||||||
comments: list[CommentDto] = []
|
comments: list[CommentDto] = []
|
||||||
for row in c.fetchall ():
|
for row in cast (list[CommentRow], c.fetchall ()):
|
||||||
comments.append (self._create_dto_from_row (row, with_relation_tables))
|
comments.append (self._create_dto_from_row (row, with_relation_tables))
|
||||||
return comments
|
return comments
|
||||||
|
|
||||||
@@ -835,6 +908,13 @@ class CommentDao:
|
|||||||
comment: CommentDto,
|
comment: CommentDto,
|
||||||
with_relation_tables: bool,
|
with_relation_tables: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
vpos_ms: int | DbNullType | None = comment.vpos_ms
|
||||||
|
if vpos_ms is None:
|
||||||
|
raise TypeError ('未実装')
|
||||||
|
if vpos_ms is DbNull:
|
||||||
|
vpos_ms = None
|
||||||
|
vpos_ms = cast (int | None, vpos_ms)
|
||||||
|
|
||||||
with self.conn.cursor (dictionary = True) as c:
|
with self.conn.cursor (dictionary = True) as c:
|
||||||
c.execute ("""
|
c.execute ("""
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
@@ -868,7 +948,7 @@ class CommentDao:
|
|||||||
comment.content,
|
comment.content,
|
||||||
comment.posted_at,
|
comment.posted_at,
|
||||||
comment.nico_count,
|
comment.nico_count,
|
||||||
comment.vpos_ms))
|
vpos_ms))
|
||||||
|
|
||||||
def upsert_all (
|
def upsert_all (
|
||||||
self,
|
self,
|
||||||
@@ -880,7 +960,7 @@ class CommentDao:
|
|||||||
|
|
||||||
def _create_dto_from_row (
|
def _create_dto_from_row (
|
||||||
self,
|
self,
|
||||||
row,
|
row: CommentRow,
|
||||||
with_relation_tables: bool,
|
with_relation_tables: bool,
|
||||||
) -> CommentDto:
|
) -> CommentDto:
|
||||||
comment = CommentDto (id_ = row['id'],
|
comment = CommentDto (id_ = row['id'],
|
||||||
@@ -890,7 +970,7 @@ class CommentDao:
|
|||||||
content = row['content'],
|
content = row['content'],
|
||||||
posted_at = row['posted_at'],
|
posted_at = row['posted_at'],
|
||||||
nico_count = row['nico_count'],
|
nico_count = row['nico_count'],
|
||||||
vpos_ms = row['vpos_ms'])
|
vpos_ms = row['vpos_ms'] or DbNull)
|
||||||
if with_relation_tables:
|
if with_relation_tables:
|
||||||
comment.video = VideoDao (self.conn).find (comment.video_id, True)
|
comment.video = VideoDao (self.conn).find (comment.video_id, True)
|
||||||
return comment
|
return comment
|
||||||
@@ -913,7 +993,7 @@ class CommentDto:
|
|||||||
class UserDao:
|
class UserDao:
|
||||||
def __init__ (
|
def __init__ (
|
||||||
self,
|
self,
|
||||||
conn,
|
conn: MySQLConnectionAbstract,
|
||||||
):
|
):
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
|
|
||||||
@@ -930,7 +1010,7 @@ class UserDao:
|
|||||||
users
|
users
|
||||||
WHERE
|
WHERE
|
||||||
code = %s""", (user_code,))
|
code = %s""", (user_code,))
|
||||||
row = c.fetchone ()
|
row = cast (UserRow | None, c.fetchone ())
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
return self._create_dto_from_row (row)
|
return self._create_dto_from_row (row)
|
||||||
@@ -949,7 +1029,7 @@ class UserDao:
|
|||||||
|
|
||||||
def _create_dto_from_row (
|
def _create_dto_from_row (
|
||||||
self,
|
self,
|
||||||
row,
|
row: UserRow,
|
||||||
) -> UserDto:
|
) -> UserDto:
|
||||||
return UserDto (id_ = row['id'],
|
return UserDto (id_ = row['id'],
|
||||||
code = row['code'])
|
code = row['code'])
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする