diff --git a/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py b/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py
new file mode 100644
index 000000000..16fb0e85e
--- /dev/null
+++ b/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py
@@ -0,0 +1,70 @@
+"""Update message & channel tables
+
+Revision ID: 3781e22d8b01
+Revises: 7826ab40b532
+Create Date: 2024-12-30 03:00:00.000000
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+revision = "3781e22d8b01"
+down_revision = "7826ab40b532"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # Add 'type' column to the 'channel' table
+ op.add_column(
+ "channel",
+ sa.Column(
+ "type",
+ sa.Text(),
+ nullable=True,
+ ),
+ )
+
+ # Add 'parent_id' column to the 'message' table for threads
+ op.add_column(
+ "message",
+ sa.Column("parent_id", sa.Text(), nullable=True),
+ )
+
+ op.create_table(
+ "message_reaction",
+ sa.Column(
+ "id", sa.Text(), nullable=False, primary_key=True, unique=True
+ ), # Unique reaction ID
+ sa.Column("user_id", sa.Text(), nullable=False), # User who reacted
+ sa.Column(
+ "message_id", sa.Text(), nullable=False
+ ), # Message that was reacted to
+ sa.Column(
+ "name", sa.Text(), nullable=False
+ ), # Reaction name (e.g. "thumbs_up")
+ sa.Column(
+ "created_at", sa.BigInteger(), nullable=True
+ ), # Timestamp of when the reaction was added
+ )
+
+ op.create_table(
+ "channel_member",
+ sa.Column(
+ "id", sa.Text(), nullable=False, primary_key=True, unique=True
+ ), # Record ID for the membership row
+ sa.Column("channel_id", sa.Text(), nullable=False), # Associated channel
+ sa.Column("user_id", sa.Text(), nullable=False), # Associated user
+ sa.Column(
+ "created_at", sa.BigInteger(), nullable=True
+ ), # Timestamp of when the user joined the channel
+ )
+
+
+def downgrade():
+ # Revert 'type' column addition to the 'channel' table
+ op.drop_column("channel", "type")
+ op.drop_column("message", "parent_id")
+ op.drop_table("message_reaction")
+ op.drop_table("channel_member")
diff --git a/backend/open_webui/models/channels.py b/backend/open_webui/models/channels.py
index bc36146cf..92f238c3a 100644
--- a/backend/open_webui/models/channels.py
+++ b/backend/open_webui/models/channels.py
@@ -21,6 +21,7 @@ class Channel(Base):
id = Column(Text, primary_key=True)
user_id = Column(Text)
+ type = Column(Text, nullable=True)
name = Column(Text)
description = Column(Text, nullable=True)
@@ -38,9 +39,11 @@ class ChannelModel(BaseModel):
id: str
user_id: str
- description: Optional[str] = None
+ type: Optional[str] = None
name: str
+ description: Optional[str] = None
+
data: Optional[dict] = None
meta: Optional[dict] = None
access_control: Optional[dict] = None
@@ -64,12 +67,13 @@ class ChannelForm(BaseModel):
class ChannelTable:
def insert_new_channel(
- self, form_data: ChannelForm, user_id: str
+ self, type: Optional[str], form_data: ChannelForm, user_id: str
) -> Optional[ChannelModel]:
with get_db() as db:
channel = ChannelModel(
**{
**form_data.model_dump(),
+ "type": type,
"name": form_data.name.lower(),
"id": str(uuid.uuid4()),
"user_id": user_id,
diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py
index 2a4322d0d..4f095a8b5 100644
--- a/backend/open_webui/models/messages.py
+++ b/backend/open_webui/models/messages.py
@@ -17,6 +17,25 @@ from sqlalchemy.sql import exists
####################
+class MessageReaction(Base):
+ __tablename__ = "message_reaction"
+ id = Column(Text, primary_key=True)
+ user_id = Column(Text)
+ message_id = Column(Text)
+ name = Column(Text)
+ created_at = Column(BigInteger)
+
+
+class MessageReactionModel(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: str
+ user_id: str
+ message_id: str
+ name: str
+ created_at: int # timestamp in epoch
+
+
class Message(Base):
__tablename__ = "message"
id = Column(Text, primary_key=True)
@@ -24,6 +43,8 @@ class Message(Base):
user_id = Column(Text)
channel_id = Column(Text, nullable=True)
+ parent_id = Column(Text, nullable=True)
+
content = Column(Text)
data = Column(JSON, nullable=True)
meta = Column(JSON, nullable=True)
@@ -39,6 +60,8 @@ class MessageModel(BaseModel):
user_id: str
channel_id: Optional[str] = None
+ parent_id: Optional[str] = None
+
content: str
data: Optional[dict] = None
meta: Optional[dict] = None
@@ -54,10 +77,23 @@ class MessageModel(BaseModel):
class MessageForm(BaseModel):
content: str
+ parent_id: Optional[str] = None
data: Optional[dict] = None
meta: Optional[dict] = None
+class Reactions(BaseModel):
+ name: str
+ user_ids: list[str]
+ count: int
+
+
+class MessageResponse(MessageModel):
+ latest_reply_at: Optional[int]
+ reply_count: int
+ reactions: list[Reactions]
+
+
class MessageTable:
def insert_new_message(
self, form_data: MessageForm, channel_id: str, user_id: str
@@ -71,6 +107,7 @@ class MessageTable:
"id": id,
"user_id": user_id,
"channel_id": channel_id,
+ "parent_id": form_data.parent_id,
"content": form_data.content,
"data": form_data.data,
"meta": form_data.meta,
@@ -85,10 +122,40 @@ class MessageTable:
db.refresh(result)
return MessageModel.model_validate(result) if result else None
- def get_message_by_id(self, id: str) -> Optional[MessageModel]:
+ def get_message_by_id(self, id: str) -> Optional[MessageResponse]:
with get_db() as db:
message = db.get(Message, id)
- return MessageModel.model_validate(message) if message else None
+ if not message:
+ return None
+
+ reactions = self.get_reactions_by_message_id(id)
+ replies = self.get_replies_by_message_id(id)
+
+ return MessageResponse(
+ **{
+ **MessageModel.model_validate(message).model_dump(),
+ "latest_reply_at": replies[0].created_at if replies else None,
+ "reply_count": len(replies),
+ "reactions": reactions,
+ }
+ )
+
+ def get_replies_by_message_id(self, id: str) -> list[MessageModel]:
+ with get_db() as db:
+ all_messages = (
+ db.query(Message)
+ .filter_by(parent_id=id)
+ .order_by(Message.created_at.desc())
+ .all()
+ )
+ return [MessageModel.model_validate(message) for message in all_messages]
+
+ def get_reply_user_ids_by_message_id(self, id: str) -> list[str]:
+ with get_db() as db:
+ return [
+ message.user_id
+ for message in db.query(Message).filter_by(parent_id=id).all()
+ ]
def get_messages_by_channel_id(
self, channel_id: str, skip: int = 0, limit: int = 50
@@ -96,7 +163,7 @@ class MessageTable:
with get_db() as db:
all_messages = (
db.query(Message)
- .filter_by(channel_id=channel_id)
+ .filter_by(channel_id=channel_id, parent_id=None)
.order_by(Message.created_at.desc())
.offset(skip)
.limit(limit)
@@ -104,19 +171,27 @@ class MessageTable:
)
return [MessageModel.model_validate(message) for message in all_messages]
- def get_messages_by_user_id(
- self, user_id: str, skip: int = 0, limit: int = 50
+ def get_messages_by_parent_id(
+ self, channel_id: str, parent_id: str, skip: int = 0, limit: int = 50
) -> list[MessageModel]:
with get_db() as db:
+ message = db.get(Message, parent_id)
+
+ if not message:
+ return []
+
all_messages = (
db.query(Message)
- .filter_by(user_id=user_id)
+ .filter_by(channel_id=channel_id, parent_id=parent_id)
.order_by(Message.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
- return [MessageModel.model_validate(message) for message in all_messages]
+
+ return [
+ MessageModel.model_validate(message) for message in all_messages
+ ] + [MessageModel.model_validate(message)]
def update_message_by_id(
self, id: str, form_data: MessageForm
@@ -131,9 +206,58 @@ class MessageTable:
db.refresh(message)
return MessageModel.model_validate(message) if message else None
+ def add_reaction_to_message(
+ self, id: str, user_id: str, name: str
+ ) -> Optional[MessageReactionModel]:
+ with get_db() as db:
+ reaction_id = str(uuid.uuid4())
+ reaction = MessageReactionModel(
+ id=reaction_id,
+ user_id=user_id,
+ message_id=id,
+ name=name,
+ created_at=int(time.time_ns()),
+ )
+ result = MessageReaction(**reaction.model_dump())
+ db.add(result)
+ db.commit()
+ db.refresh(result)
+ return MessageReactionModel.model_validate(result) if result else None
+
+ def get_reactions_by_message_id(self, id: str) -> list[Reactions]:
+ with get_db() as db:
+ all_reactions = db.query(MessageReaction).filter_by(message_id=id).all()
+
+ reactions = {}
+ for reaction in all_reactions:
+ if reaction.name not in reactions:
+ reactions[reaction.name] = {
+ "name": reaction.name,
+ "user_ids": [],
+ "count": 0,
+ }
+ reactions[reaction.name]["user_ids"].append(reaction.user_id)
+ reactions[reaction.name]["count"] += 1
+
+ return [Reactions(**reaction) for reaction in reactions.values()]
+
+ def remove_reaction_by_id_and_user_id_and_name(
+ self, id: str, user_id: str, name: str
+ ) -> bool:
+ with get_db() as db:
+ db.query(MessageReaction).filter_by(
+ message_id=id, user_id=user_id, name=name
+ ).delete()
+ db.commit()
+ return True
+
def delete_message_by_id(self, id: str) -> bool:
with get_db() as db:
db.query(Message).filter_by(id=id).delete()
+
+ # Delete all reactions to this message
+ db.query(MessageReaction).filter_by(message_id=id).delete()
+
db.commit()
return True
diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py
index 292f33e78..8089be68f 100644
--- a/backend/open_webui/routers/channels.py
+++ b/backend/open_webui/routers/channels.py
@@ -11,7 +11,12 @@ from open_webui.socket.main import sio, get_user_ids_from_room
from open_webui.models.users import Users, UserNameResponse
from open_webui.models.channels import Channels, ChannelModel, ChannelForm
-from open_webui.models.messages import Messages, MessageModel, MessageForm
+from open_webui.models.messages import (
+ Messages,
+ MessageModel,
+ MessageResponse,
+ MessageForm,
+)
from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
@@ -49,7 +54,7 @@ async def get_channels(user=Depends(get_verified_user)):
@router.post("/create", response_model=Optional[ChannelModel])
async def create_new_channel(form_data: ChannelForm, user=Depends(get_admin_user)):
try:
- channel = Channels.insert_new_channel(form_data, user.id)
+ channel = Channels.insert_new_channel(None, form_data, user.id)
return ChannelModel(**channel.model_dump())
except Exception as e:
log.exception(e)
@@ -134,11 +139,11 @@ async def delete_channel_by_id(id: str, user=Depends(get_admin_user)):
############################
-class MessageUserModel(MessageModel):
+class MessageUserResponse(MessageResponse):
user: UserNameResponse
-@router.get("/{id}/messages", response_model=list[MessageUserModel])
+@router.get("/{id}/messages", response_model=list[MessageUserResponse])
async def get_channel_messages(
id: str, skip: int = 0, limit: int = 50, user=Depends(get_verified_user)
):
@@ -164,10 +169,16 @@ async def get_channel_messages(
user = Users.get_user_by_id(message.user_id)
users[message.user_id] = user
+ replies = Messages.get_replies_by_message_id(message.id)
+ latest_reply_at = replies[0].created_at if replies else None
+
messages.append(
- MessageUserModel(
+ MessageUserResponse(
**{
**message.model_dump(),
+ "reply_count": len(replies),
+ "latest_reply_at": latest_reply_at,
+ "reactions": Messages.get_reactions_by_message_id(message.id),
"user": UserNameResponse(**users[message.user_id].model_dump()),
}
)
@@ -236,10 +247,17 @@ async def post_new_message(
"message_id": message.id,
"data": {
"type": "message",
- "data": {
- **message.model_dump(),
- "user": UserNameResponse(**user.model_dump()).model_dump(),
- },
+ "data": MessageUserResponse(
+ **{
+ **message.model_dump(),
+ "reply_count": 0,
+ "latest_reply_at": None,
+ "reactions": Messages.get_reactions_by_message_id(
+ message.id
+ ),
+ "user": UserNameResponse(**user.model_dump()),
+ }
+ ).model_dump(),
},
"user": UserNameResponse(**user.model_dump()).model_dump(),
"channel": channel.model_dump(),
@@ -251,6 +269,35 @@ async def post_new_message(
to=f"channel:{channel.id}",
)
+ if message.parent_id:
+ # If this message is a reply, emit to the parent message as well
+ parent_message = Messages.get_message_by_id(message.parent_id)
+
+ if parent_message:
+ await sio.emit(
+ "channel-events",
+ {
+ "channel_id": channel.id,
+ "message_id": parent_message.id,
+ "data": {
+ "type": "message:reply",
+ "data": MessageUserResponse(
+ **{
+ **parent_message.model_dump(),
+ "user": UserNameResponse(
+ **Users.get_user_by_id(
+ parent_message.user_id
+ ).model_dump()
+ ),
+ }
+ ).model_dump(),
+ },
+ "user": UserNameResponse(**user.model_dump()).model_dump(),
+ "channel": channel.model_dump(),
+ },
+ to=f"channel:{channel.id}",
+ )
+
active_user_ids = get_user_ids_from_room(f"channel:{channel.id}")
background_tasks.add_task(
@@ -269,6 +316,101 @@ async def post_new_message(
)
+############################
+# GetChannelMessage
+############################
+
+
+@router.get("/{id}/messages/{message_id}", response_model=Optional[MessageUserResponse])
+async def get_channel_message(
+ id: str, message_id: str, user=Depends(get_verified_user)
+):
+ channel = Channels.get_channel_by_id(id)
+ if not channel:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
+ )
+
+ if user.role != "admin" and not has_access(
+ user.id, type="read", access_control=channel.access_control
+ ):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+ message = Messages.get_message_by_id(message_id)
+ if not message:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
+ )
+
+ if message.channel_id != id:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+ return MessageUserResponse(
+ **{
+ **message.model_dump(),
+ "user": UserNameResponse(
+ **Users.get_user_by_id(message.user_id).model_dump()
+ ),
+ }
+ )
+
+
+############################
+# GetChannelThreadMessages
+############################
+
+
+@router.get(
+ "/{id}/messages/{message_id}/thread", response_model=list[MessageUserResponse]
+)
+async def get_channel_thread_messages(
+ id: str,
+ message_id: str,
+ skip: int = 0,
+ limit: int = 50,
+ user=Depends(get_verified_user),
+):
+ channel = Channels.get_channel_by_id(id)
+ if not channel:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
+ )
+
+ if user.role != "admin" and not has_access(
+ user.id, type="read", access_control=channel.access_control
+ ):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+ message_list = Messages.get_messages_by_parent_id(id, message_id, skip, limit)
+ users = {}
+
+ messages = []
+ for message in message_list:
+ if message.user_id not in users:
+ user = Users.get_user_by_id(message.user_id)
+ users[message.user_id] = user
+
+ messages.append(
+ MessageUserResponse(
+ **{
+ **message.model_dump(),
+ "reply_count": 0,
+ "latest_reply_at": None,
+ "reactions": Messages.get_reactions_by_message_id(message.id),
+ "user": UserNameResponse(**users[message.user_id].model_dump()),
+ }
+ )
+ )
+
+ return messages
+
+
############################
# UpdateMessageById
############################
@@ -306,6 +448,8 @@ async def update_message_by_id(
try:
message = Messages.update_message_by_id(message_id, form_data)
+ message = Messages.get_message_by_id(message_id)
+
if message:
await sio.emit(
"channel-events",
@@ -314,10 +458,14 @@ async def update_message_by_id(
"message_id": message.id,
"data": {
"type": "message:update",
- "data": {
- **message.model_dump(),
- "user": UserNameResponse(**user.model_dump()).model_dump(),
- },
+ "data": MessageUserResponse(
+ **{
+ **message.model_dump(),
+ "user": UserNameResponse(
+ **user.model_dump()
+ ).model_dump(),
+ }
+ ).model_dump(),
},
"user": UserNameResponse(**user.model_dump()).model_dump(),
"channel": channel.model_dump(),
@@ -333,6 +481,145 @@ async def update_message_by_id(
)
+############################
+# AddReactionToMessage
+############################
+
+
+class ReactionForm(BaseModel):
+ name: str
+
+
+@router.post("/{id}/messages/{message_id}/reactions/add", response_model=bool)
+async def add_reaction_to_message(
+ id: str, message_id: str, form_data: ReactionForm, user=Depends(get_verified_user)
+):
+ channel = Channels.get_channel_by_id(id)
+ if not channel:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
+ )
+
+ if user.role != "admin" and not has_access(
+ user.id, type="read", access_control=channel.access_control
+ ):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+ message = Messages.get_message_by_id(message_id)
+ if not message:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
+ )
+
+ if message.channel_id != id:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+ try:
+ Messages.add_reaction_to_message(message_id, user.id, form_data.name)
+ message = Messages.get_message_by_id(message_id)
+
+ await sio.emit(
+ "channel-events",
+ {
+ "channel_id": channel.id,
+ "message_id": message.id,
+ "data": {
+ "type": "message:reaction:add",
+ "data": {
+ **message.model_dump(),
+ "user": UserNameResponse(
+ **Users.get_user_by_id(message.user_id).model_dump()
+ ).model_dump(),
+ "name": form_data.name,
+ },
+ },
+ "user": UserNameResponse(**user.model_dump()).model_dump(),
+ "channel": channel.model_dump(),
+ },
+ to=f"channel:{channel.id}",
+ )
+
+ return True
+ except Exception as e:
+ log.exception(e)
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+
+############################
+# RemoveReactionById
+############################
+
+
+@router.post("/{id}/messages/{message_id}/reactions/remove", response_model=bool)
+async def remove_reaction_by_id_and_user_id_and_name(
+ id: str, message_id: str, form_data: ReactionForm, user=Depends(get_verified_user)
+):
+ channel = Channels.get_channel_by_id(id)
+ if not channel:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
+ )
+
+ if user.role != "admin" and not has_access(
+ user.id, type="read", access_control=channel.access_control
+ ):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+ message = Messages.get_message_by_id(message_id)
+ if not message:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
+ )
+
+ if message.channel_id != id:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+ try:
+ Messages.remove_reaction_by_id_and_user_id_and_name(
+ message_id, user.id, form_data.name
+ )
+
+ message = Messages.get_message_by_id(message_id)
+
+ await sio.emit(
+ "channel-events",
+ {
+ "channel_id": channel.id,
+ "message_id": message.id,
+ "data": {
+ "type": "message:reaction:remove",
+ "data": {
+ **message.model_dump(),
+ "user": UserNameResponse(
+ **Users.get_user_by_id(message.user_id).model_dump()
+ ).model_dump(),
+ "name": form_data.name,
+ },
+ },
+ "user": UserNameResponse(**user.model_dump()).model_dump(),
+ "channel": channel.model_dump(),
+ },
+ to=f"channel:{channel.id}",
+ )
+
+ return True
+ except Exception as e:
+ log.exception(e)
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+
############################
# DeleteMessageById
############################
diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py
index f3e9a033e..2d12f5803 100644
--- a/backend/open_webui/socket/main.py
+++ b/backend/open_webui/socket/main.py
@@ -237,6 +237,7 @@ async def channel_events(sid, data):
"channel-events",
{
"channel_id": data["channel_id"],
+ "message_id": data.get("message_id", None),
"data": event_data,
"user": UserNameResponse(**SESSION_POOL[sid]).model_dump(),
},
diff --git a/package.json b/package.json
index ed988fb60..71de337da 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,6 @@
"type": "module",
"dependencies": {
"@codemirror/lang-javascript": "^6.2.2",
- "codemirror-lang-hcl": "^0.0.0-beta.2",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/language-data": "^6.5.1",
"@codemirror/theme-one-dark": "^6.1.2",
@@ -69,6 +68,7 @@
"async": "^3.2.5",
"bits-ui": "^0.19.7",
"codemirror": "^6.0.1",
+ "codemirror-lang-hcl": "^0.0.0-beta.2",
"crc-32": "^1.2.2",
"dayjs": "^1.11.10",
"dompurify": "^3.1.6",
diff --git a/src/lib/apis/channels/index.ts b/src/lib/apis/channels/index.ts
index 607a8f900..965d24331 100644
--- a/src/lib/apis/channels/index.ts
+++ b/src/lib/apis/channels/index.ts
@@ -1,4 +1,5 @@
import { WEBUI_API_BASE_URL } from '$lib/constants';
+import { t } from 'i18next';
type ChannelForm = {
name: string;
@@ -207,7 +208,49 @@ export const getChannelMessages = async (
return res;
};
+
+export const getChannelThreadMessages = async (
+ token: string = '',
+ channel_id: string,
+ message_id: string,
+ skip: number = 0,
+ limit: number = 50
+) => {
+ let error = null;
+
+ const res = await fetch(
+ `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/thread?skip=${skip}&limit=${limit}`,
+ {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ }
+ }
+ )
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .then((json) => {
+ return json;
+ })
+ .catch((err) => {
+ error = err.detail;
+ console.log(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+}
+
type MessageForm = {
+ parent_id?: string;
content: string;
data?: object;
meta?: object;
@@ -285,6 +328,77 @@ export const updateMessage = async (
return res;
};
+export const addReaction = async (token: string = '', channel_id: string, message_id: string, name: string) => {
+ let error = null;
+
+ const res = await fetch(
+ `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/reactions/add`,
+ {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ },
+ body: JSON.stringify({ name })
+ }
+ )
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .then((json) => {
+ return json;
+ })
+ .catch((err) => {
+ error = err.detail;
+ console.log(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+}
+
+
+export const removeReaction = async (token: string = '', channel_id: string, message_id: string, name: string) => {
+ let error = null;
+
+ const res = await fetch(
+ `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/reactions/remove`,
+ {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ },
+ body: JSON.stringify({ name })
+ }
+ )
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .then((json) => {
+ return json;
+ })
+ .catch((err) => {
+ error = err.detail;
+ console.log(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+}
+
export const deleteMessage = async (token: string = '', channel_id: string, message_id: string) => {
let error = null;
diff --git a/src/lib/components/channel/Channel.svelte b/src/lib/components/channel/Channel.svelte
index a27c88540..e83e37c08 100644
--- a/src/lib/components/channel/Channel.svelte
+++ b/src/lib/components/channel/Channel.svelte
@@ -1,14 +1,19 @@
+
+
+
+