diff --git a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api
index 5c7ed5577..accd37aee 100644
--- a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api
+++ b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api
@@ -362,6 +362,7 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin
 	public abstract fun getSettings ()Lnet/mamoe/mirai/contact/GroupSettings;
 	public fun quit ()Z
 	public abstract fun quit (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static synthetic fun sendAnnouncement$default (Lnet/mamoe/mirai/contact/Group$Companion;Lnet/mamoe/mirai/contact/Group;Ljava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/data/AnnouncementParameters;ILjava/lang/Object;)V
 	public fun sendMessage (Ljava/lang/String;)Lnet/mamoe/mirai/message/MessageReceipt;
 	public fun sendMessage (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun sendMessage (Lnet/mamoe/mirai/message/data/Message;)Lnet/mamoe/mirai/message/MessageReceipt;
@@ -376,6 +377,8 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin
 }
 
 public final class net/mamoe/mirai/contact/Group$Companion {
+	public static synthetic fun sendAnnouncement$default (Lnet/mamoe/mirai/contact/Group$Companion;Lnet/mamoe/mirai/contact/Group;Ljava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/data/AnnouncementParameters;ILjava/lang/Object;)V
+	public static synthetic fun sendAnnouncement$default (Lnet/mamoe/mirai/contact/Group$Companion;Lnet/mamoe/mirai/contact/Group;Ljava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/data/AnnouncementParameters;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
 	public final fun setEssenceMessage (Lnet/mamoe/mirai/contact/Group;Lnet/mamoe/mirai/message/data/MessageChain;)Z
 	public final fun setEssenceMessage (Lnet/mamoe/mirai/contact/Group;Lnet/mamoe/mirai/message/data/MessageChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 }
@@ -569,6 +572,62 @@ public abstract interface class net/mamoe/mirai/contact/UserOrBot : net/mamoe/mi
 	public abstract fun nudge ()Lnet/mamoe/mirai/message/action/Nudge;
 }
 
+public class net/mamoe/mirai/data/Announcement {
+	public static final field Companion Lnet/mamoe/mirai/data/Announcement$Companion;
+	public static final fun create (JLjava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/data/AnnouncementParameters;)Lnet/mamoe/mirai/data/Announcement;
+	public final fun getBotId ()J
+	public final fun getMsg ()Ljava/lang/String;
+	public final fun getParameters ()Lnet/mamoe/mirai/data/AnnouncementParameters;
+	public final fun getTitle ()Ljava/lang/String;
+	public synthetic fun publish (Lnet/mamoe/mirai/contact/Group;)Lkotlin/Unit;
+	public fun publish (Lnet/mamoe/mirai/contact/Group;)V
+	public final fun publish (Lnet/mamoe/mirai/contact/Group;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class net/mamoe/mirai/data/Announcement$Companion {
+	public final fun create (JLjava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/data/AnnouncementParameters;)Lnet/mamoe/mirai/data/Announcement;
+}
+
+public final class net/mamoe/mirai/data/AnnouncementKt {
+	public static final fun buildAnnouncementParameters (Lkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/data/AnnouncementParameters;
+}
+
+public final class net/mamoe/mirai/data/AnnouncementParameters {
+	public fun <init> ()V
+	public final fun builder ()Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun getImage ()[B
+	public final fun getNeedConfirm ()Z
+	public final fun getSendToNewMember ()Z
+	public final fun isPinned ()Z
+	public final fun isShowEditCard ()Z
+	public final fun isTip ()Z
+}
+
+public final class net/mamoe/mirai/data/AnnouncementParametersBuilder {
+	public fun <init> ()V
+	public fun <init> (Lnet/mamoe/mirai/data/AnnouncementParameters;)V
+	public synthetic fun <init> (Lnet/mamoe/mirai/data/AnnouncementParameters;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public final fun build ()Lnet/mamoe/mirai/data/AnnouncementParameters;
+	public final fun getImage ()[B
+	public final fun getNeedConfirm ()Z
+	public final fun getSendToNewMember ()Z
+	public final fun image ([B)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun isPinned ()Z
+	public final fun isShowEditCard ()Z
+	public final fun isTip ()Z
+	public final fun needConfirm (Z)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun pinned (Z)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun sendToNewMember (Z)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun setImage ([B)V
+	public final fun setNeedConfirm (Z)V
+	public final fun setPinned (Z)V
+	public final fun setSendToNewMember (Z)V
+	public final fun setShowEditCard (Z)V
+	public final fun setTip (Z)V
+	public final fun showEditCard (Z)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun tip (Z)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+}
+
 public abstract interface class net/mamoe/mirai/data/FriendInfo : net/mamoe/mirai/data/UserInfo {
 	public abstract fun getNick ()Ljava/lang/String;
 	public abstract fun getRemark ()Ljava/lang/String;
@@ -870,6 +929,21 @@ public final class net/mamoe/mirai/data/GroupAnnouncement$Companion {
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
+public final class net/mamoe/mirai/data/GroupAnnouncementImage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+	public static final field INSTANCE Lnet/mamoe/mirai/data/GroupAnnouncementImage$$serializer;
+	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/data/GroupAnnouncementImage;
+	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/data/GroupAnnouncementImage;)V
+	public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class net/mamoe/mirai/data/GroupAnnouncementImage$Companion {
+	public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
 public final class net/mamoe/mirai/data/GroupAnnouncementList$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/data/GroupAnnouncementList$$serializer;
 	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
@@ -1232,6 +1306,14 @@ public final class net/mamoe/mirai/data/OnlineStatus$Companion {
 	public final fun ofIdOrNull (I)Lnet/mamoe/mirai/data/OnlineStatus;
 }
 
+public final class net/mamoe/mirai/data/ReceiveAnnouncement : net/mamoe/mirai/data/Announcement {
+	public final fun getFid ()Ljava/lang/String;
+	public final fun getPublishTime ()J
+	public final fun getReadMemberNumber ()I
+	public final fun getSenderId ()J
+	public final fun isAllRead ()Z
+}
+
 public abstract interface class net/mamoe/mirai/data/StrangerInfo : net/mamoe/mirai/data/UserInfo {
 	public abstract fun getFromGroup ()J
 	public abstract fun getNick ()Ljava/lang/String;
diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api
index 393e1be87..0c44154bb 100644
--- a/binary-compatibility-validator/api/binary-compatibility-validator.api
+++ b/binary-compatibility-validator/api/binary-compatibility-validator.api
@@ -362,6 +362,7 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin
 	public abstract fun getSettings ()Lnet/mamoe/mirai/contact/GroupSettings;
 	public fun quit ()Z
 	public abstract fun quit (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static synthetic fun sendAnnouncement$default (Lnet/mamoe/mirai/contact/Group$Companion;Lnet/mamoe/mirai/contact/Group;Ljava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/data/AnnouncementParameters;ILjava/lang/Object;)V
 	public fun sendMessage (Ljava/lang/String;)Lnet/mamoe/mirai/message/MessageReceipt;
 	public fun sendMessage (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun sendMessage (Lnet/mamoe/mirai/message/data/Message;)Lnet/mamoe/mirai/message/MessageReceipt;
@@ -376,6 +377,8 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin
 }
 
 public final class net/mamoe/mirai/contact/Group$Companion {
+	public static synthetic fun sendAnnouncement$default (Lnet/mamoe/mirai/contact/Group$Companion;Lnet/mamoe/mirai/contact/Group;Ljava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/data/AnnouncementParameters;ILjava/lang/Object;)V
+	public static synthetic fun sendAnnouncement$default (Lnet/mamoe/mirai/contact/Group$Companion;Lnet/mamoe/mirai/contact/Group;Ljava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/data/AnnouncementParameters;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
 	public final fun setEssenceMessage (Lnet/mamoe/mirai/contact/Group;Lnet/mamoe/mirai/message/data/MessageChain;)Z
 	public final fun setEssenceMessage (Lnet/mamoe/mirai/contact/Group;Lnet/mamoe/mirai/message/data/MessageChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 }
@@ -569,6 +572,62 @@ public abstract interface class net/mamoe/mirai/contact/UserOrBot : net/mamoe/mi
 	public abstract fun nudge ()Lnet/mamoe/mirai/message/action/Nudge;
 }
 
+public class net/mamoe/mirai/data/Announcement {
+	public static final field Companion Lnet/mamoe/mirai/data/Announcement$Companion;
+	public static final fun create (JLjava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/data/AnnouncementParameters;)Lnet/mamoe/mirai/data/Announcement;
+	public final fun getBotId ()J
+	public final fun getMsg ()Ljava/lang/String;
+	public final fun getParameters ()Lnet/mamoe/mirai/data/AnnouncementParameters;
+	public final fun getTitle ()Ljava/lang/String;
+	public synthetic fun publish (Lnet/mamoe/mirai/contact/Group;)Lkotlin/Unit;
+	public fun publish (Lnet/mamoe/mirai/contact/Group;)V
+	public final fun publish (Lnet/mamoe/mirai/contact/Group;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class net/mamoe/mirai/data/Announcement$Companion {
+	public final fun create (JLjava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/data/AnnouncementParameters;)Lnet/mamoe/mirai/data/Announcement;
+}
+
+public final class net/mamoe/mirai/data/AnnouncementKt {
+	public static final fun buildAnnouncementParameters (Lkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/data/AnnouncementParameters;
+}
+
+public final class net/mamoe/mirai/data/AnnouncementParameters {
+	public fun <init> ()V
+	public final fun builder ()Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun getImage ()[B
+	public final fun getNeedConfirm ()Z
+	public final fun getSendToNewMember ()Z
+	public final fun isPinned ()Z
+	public final fun isShowEditCard ()Z
+	public final fun isTip ()Z
+}
+
+public final class net/mamoe/mirai/data/AnnouncementParametersBuilder {
+	public fun <init> ()V
+	public fun <init> (Lnet/mamoe/mirai/data/AnnouncementParameters;)V
+	public synthetic fun <init> (Lnet/mamoe/mirai/data/AnnouncementParameters;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public final fun build ()Lnet/mamoe/mirai/data/AnnouncementParameters;
+	public final fun getImage ()[B
+	public final fun getNeedConfirm ()Z
+	public final fun getSendToNewMember ()Z
+	public final fun image ([B)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun isPinned ()Z
+	public final fun isShowEditCard ()Z
+	public final fun isTip ()Z
+	public final fun needConfirm (Z)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun pinned (Z)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun sendToNewMember (Z)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun setImage ([B)V
+	public final fun setNeedConfirm (Z)V
+	public final fun setPinned (Z)V
+	public final fun setSendToNewMember (Z)V
+	public final fun setShowEditCard (Z)V
+	public final fun setTip (Z)V
+	public final fun showEditCard (Z)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+	public final fun tip (Z)Lnet/mamoe/mirai/data/AnnouncementParametersBuilder;
+}
+
 public abstract interface class net/mamoe/mirai/data/FriendInfo : net/mamoe/mirai/data/UserInfo {
 	public abstract fun getNick ()Ljava/lang/String;
 	public abstract fun getRemark ()Ljava/lang/String;
@@ -870,6 +929,21 @@ public final class net/mamoe/mirai/data/GroupAnnouncement$Companion {
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
+public final class net/mamoe/mirai/data/GroupAnnouncementImage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+	public static final field INSTANCE Lnet/mamoe/mirai/data/GroupAnnouncementImage$$serializer;
+	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/data/GroupAnnouncementImage;
+	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/data/GroupAnnouncementImage;)V
+	public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class net/mamoe/mirai/data/GroupAnnouncementImage$Companion {
+	public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
 public final class net/mamoe/mirai/data/GroupAnnouncementList$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/data/GroupAnnouncementList$$serializer;
 	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
@@ -1232,6 +1306,14 @@ public final class net/mamoe/mirai/data/OnlineStatus$Companion {
 	public final fun ofIdOrNull (I)Lnet/mamoe/mirai/data/OnlineStatus;
 }
 
+public final class net/mamoe/mirai/data/ReceiveAnnouncement : net/mamoe/mirai/data/Announcement {
+	public final fun getFid ()Ljava/lang/String;
+	public final fun getPublishTime ()J
+	public final fun getReadMemberNumber ()I
+	public final fun getSenderId ()J
+	public final fun isAllRead ()Z
+}
+
 public abstract interface class net/mamoe/mirai/data/StrangerInfo : net/mamoe/mirai/data/UserInfo {
 	public abstract fun getFromGroup ()J
 	public abstract fun getNick ()Ljava/lang/String;
diff --git a/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt b/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt
index 0417208e3..caad5c00a 100644
--- a/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt
+++ b/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt
@@ -15,6 +15,7 @@ import kotlinx.coroutines.Job
 import net.mamoe.kjbb.JvmBlockingBridge
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.*
+import net.mamoe.mirai.utils.ExternalResource
 import net.mamoe.mirai.utils.MiraiExperimentalApi
 import net.mamoe.mirai.utils.WeakRef
 import kotlin.annotation.AnnotationTarget.*
@@ -141,6 +142,18 @@ public interface LowLevelApiAccessor {
         amount: Int = 10
     ): GroupAnnouncementList
 
+    /**
+     * 上传群公告的所需要的一个图片,但不发送
+     *
+     */
+    @LowLevelApi
+    @MiraiExperimentalApi
+    public suspend fun uploadGroupAnnouncementImage(
+        bot: Bot,
+        groupId: Long,
+        resource: ExternalResource
+    ): GroupAnnouncementImage
+
     /**
      * 发送群公告
      *
@@ -154,6 +167,19 @@ public interface LowLevelApiAccessor {
         announcement: GroupAnnouncement
     ): String
 
+    /**
+     * 发送包含图片的群公告
+     *
+     * @return 公告的fid
+     */
+    @LowLevelApi
+    @MiraiExperimentalApi
+    public suspend fun sendGroupAnnouncementWithImage(
+        bot: Bot,
+        groupId: Long,
+        image: GroupAnnouncementImage,
+        announcement: GroupAnnouncement
+    ):String
 
     /**
      * 删除群公告
diff --git a/mirai-core-api/src/commonMain/kotlin/contact/Group.kt b/mirai-core-api/src/commonMain/kotlin/contact/Group.kt
index cc00b48af..497fe1421 100644
--- a/mirai-core-api/src/commonMain/kotlin/contact/Group.kt
+++ b/mirai-core-api/src/commonMain/kotlin/contact/Group.kt
@@ -12,8 +12,14 @@
 package net.mamoe.mirai.contact
 
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
 import net.mamoe.kjbb.JvmBlockingBridge
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.Mirai
+import net.mamoe.mirai.data.Announcement
+import net.mamoe.mirai.data.AnnouncementParameters
+import net.mamoe.mirai.data.ReceiveAnnouncement
+import net.mamoe.mirai.data.covertToGroupAnnouncement
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.*
@@ -175,6 +181,28 @@ public interface Group : Contact, CoroutineScope, FileSupported {
      */
     public suspend fun setEssenceMessage(source: MessageSource): Boolean
 
+    /**
+     * 获取所有群公告列表
+     */
+    @MiraiExperimentalApi
+    public suspend fun getAnnouncements(): Flow<Announcement>
+
+    /**
+     * 删除一条群公告
+     * @param fid 公告的id [ReceiveAnnouncement.fid]
+     *
+     * @throws PermissionDeniedException 没有权限时抛出
+     */
+    @MiraiExperimentalApi
+    public suspend fun deleteAnnouncement(fid: String)
+
+    /**
+     * 获取一条群公告
+     * @param fid 公告的id [ReceiveAnnouncement.fid]
+     */
+    @MiraiExperimentalApi
+    public suspend fun getAnnouncement(fid: String): ReceiveAnnouncement
+
     public companion object {
         /**
          * 将一条消息设置为群精华消息, 需要管理员或群主权限.
@@ -188,6 +216,39 @@ public interface Group : Contact, CoroutineScope, FileSupported {
         @JvmBlockingBridge
         @JvmStatic
         public suspend fun Group.setEssenceMessage(chain: MessageChain): Boolean = setEssenceMessage(chain.source)
+
+        /**
+         * 发送一个 [Announcement]
+         *
+         * @param title 公告标题
+         * @param msg 公告内容
+         * @param announcementParameters 公告设置
+         */
+        @MiraiExperimentalApi
+        @JvmBlockingBridge
+        @JvmStatic
+        public suspend fun Group.sendAnnouncement(
+            title: String,
+            msg: String,
+            announcementParameters: AnnouncementParameters = AnnouncementParameters()
+        ) {
+            checkBotPermission(MemberPermission.ADMINISTRATOR) { "Only administrator have permission to send group announcement" }
+            Mirai.sendGroupAnnouncement(
+                bot,
+                id,
+                Announcement(bot.id, title, msg, announcementParameters).covertToGroupAnnouncement()
+            )
+        }
+
+        /**
+         * 删除一条群公告
+         * @param receiveAnnouncement 公告 [ReceiveAnnouncement]
+         */
+        @MiraiExperimentalApi
+        @JvmBlockingBridge
+        @JvmStatic
+        public suspend fun Group.deleteAnnouncement(receiveAnnouncement: ReceiveAnnouncement): Unit =
+            deleteAnnouncement(receiveAnnouncement.fid)
     }
 }
 
@@ -258,3 +319,4 @@ public inline fun Group.getMemberOrFail(id: Long): NormalMember = getOrFail(id)
  * @see Group.botMuteRemaining 剩余禁言时间
  */
 public inline val Group.isBotMuted: Boolean get() = this.botMuteRemaining != 0
+
diff --git a/mirai-core-api/src/commonMain/kotlin/data/Announcement.kt b/mirai-core-api/src/commonMain/kotlin/data/Announcement.kt
new file mode 100644
index 000000000..52e24dc69
--- /dev/null
+++ b/mirai-core-api/src/commonMain/kotlin/data/Announcement.kt
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ *  此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ *  Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ *  https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+@file:Suppress("unused")
+@file:JvmBlockingBridge
+
+package net.mamoe.mirai.data
+
+import net.mamoe.kjbb.JvmBlockingBridge
+import net.mamoe.mirai.Mirai
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.MemberPermission
+import net.mamoe.mirai.contact.checkBotPermission
+import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import java.time.Instant
+
+/**
+ * 表示一个群公告. [ReceiveAnnouncement] 表示
+ *
+ * 可通过 [Announcement.create] 构造.
+ *
+ * @see Announcement
+ *
+ * @since 2.7
+ */
+public open class Announcement internal constructor(
+    /**
+     * bot的Id
+     */
+    public val botId: Long,
+
+    /**
+     * 公告的标题
+     */
+    public val title: String,
+
+    /**
+     * 公告的内容
+     */
+    public val msg: String,
+
+    /**
+     * 公告的可变参数. 可以通过 [AnnouncementParametersBuilder] 构建获得.
+     * @see AnnouncementParameters
+     * @see AnnouncementParametersBuilder
+     */
+    public val parameters: AnnouncementParameters
+) {
+    /**
+     * 发送该公告到群
+     */
+    public suspend fun publish(group: Group) {
+        val bot = group.bot
+        group.checkBotPermission(MemberPermission.ADMINISTRATOR) { "Only administrator have permission to send group announcement" }
+        if (parameters.image == null)
+            Mirai.sendGroupAnnouncement(bot, group.id, covertToGroupAnnouncement())
+        else {
+            parameters.image.toExternalResource().use {
+                val image =
+                    Mirai.uploadGroupAnnouncementImage(bot, group.id, it)
+                Mirai.sendGroupAnnouncementWithImage(bot, group.id, image, covertToGroupAnnouncement())
+            }
+        }
+    }
+
+    public companion object {
+        /**
+         * 构造一个 [Announcement].
+         * @see Announcement
+         */
+        @JvmStatic
+        public fun create(botId: Long, title: String, msg: String, parameters: AnnouncementParameters): Announcement {
+            return Announcement(botId, title, msg, parameters)
+        }
+    }
+}
+
+/**
+ * 群公告的扩展参数.
+ *
+ * 可通过 [AnnouncementParametersBuilder] 构建. [AnnouncementParameters] 用于 [创建公告][Announcement.create].
+ *
+ * @since 2.7
+ */
+public class AnnouncementParameters internal constructor(
+    /**
+     * 群公告的图片,目前仅支持发送图片,不支持获得图片
+     */
+    public val image: ByteArray? = null,
+
+    /**
+     * 是否发送给新成员
+     */
+    public val sendToNewMember: Boolean = false,
+
+    /**
+     * 是否置顶,可以有多个置顶公告
+     */
+    public val isPinned: Boolean = false,
+
+    /**
+     * 是否显示能够引导群成员修改昵称的窗口
+     */
+    public val isShowEditCard: Boolean = false,
+
+    /**
+     * 是否使用弹窗
+     */
+    public val isTip: Boolean = false,
+
+    /**
+     * 是否需要群成员确认
+     */
+    public val needConfirm: Boolean = false,
+) {
+    /**
+     * 以该对象的参数创建一个 [AnnouncementParametersBuilder].
+     */
+    public fun builder(): AnnouncementParametersBuilder = AnnouncementParametersBuilder().apply {
+        val outer = this@AnnouncementParameters
+        this.image = outer.image
+        this.sendToNewMember = outer.sendToNewMember
+        this.isPinned = outer.isPinned
+        this.isShowEditCard = outer.isShowEditCard
+        this.isTip = outer.isTip
+        this.needConfirm = outer.needConfirm
+    }
+}
+
+/**
+ * 表示一个收到的群公告. 只能由 mirai 构造.
+ *
+ * @since 2.7
+ */
+public class ReceiveAnnouncement internal constructor(
+    /**
+     * bot的 Id
+     */
+    botId: Long,
+    /**
+     * 公告的标题
+     */
+    title: String,
+    /**
+     * 公告的内容
+     */
+    msg: String,
+    /**
+     * 公告的可变参数
+     */
+    parameters: AnnouncementParameters,
+    /**
+     * 公告发送者的 QQ 号
+     */
+    public val senderId: Long,
+
+    /**
+     * 公告的 `fid,每个公告仅有一条 `fid`,类似于主键
+     */
+    public val fid: String,
+
+    /**
+     * 所有人都已阅读, 如果 [AnnouncementParameters.needConfirm] 为 `true` 则为所有人都已确认,
+     */
+    public val isAllRead: Boolean,
+
+    /**
+     * 已经阅读的成员数量,如果 [AnnouncementParameters.needConfirm] 为 `true` 则为已经确认的成员数量
+     */
+    public val readMemberNumber: Int,
+
+    /**
+     * 公告发出的时间,为 EpochSecond (自 1970-01-01T00:00:00Z 的秒数)
+     *
+     * @see Instant.ofEpochSecond
+     */
+    public val publishTime: Long,
+) : Announcement(botId, title, msg, parameters)
+
+/**
+ * [AnnouncementParameters] 的构建器. 可以构建一个 [AnnouncementParameters] 实例.
+ *
+ * ## 获得实例
+ *
+ * 直接构造实例: `new AnnouncementParametersBuilder()` 或者从已有的公告中获取 [AnnouncementParameters.builder].
+ *
+ * ## 使用
+ *
+ * ### 在 Kotlin 使用
+ *
+ * ```
+ * val parameters = buildAnnouncementParameters {
+ *     sendToNewMember = true
+ *     // ...
+ * }
+ * ```
+ *
+ * ### 在 Java 使用
+ *
+ * ```java
+ * AnnouncementParameters parameters = new AnnouncementParametersBuilder()
+ *         .sendToNewMember(true)
+ *         .pinned(true)
+ *         .build();
+ * ```
+ *
+ * @see buildAnnouncementParameters
+ *
+ * @since 2.7
+ */
+public class AnnouncementParametersBuilder @JvmOverloads constructor(
+    prototype: AnnouncementParameters = AnnouncementParameters()
+) {
+    public var image: ByteArray? = prototype.image
+    public var sendToNewMember: Boolean = prototype.sendToNewMember
+    public var isPinned: Boolean = prototype.isPinned
+    public var isShowEditCard: Boolean = prototype.isShowEditCard
+    public var isTip: Boolean = prototype.isTip
+    public var needConfirm: Boolean = prototype.needConfirm
+
+    public fun image(image: ByteArray?): AnnouncementParametersBuilder {
+        this.image = image
+        return this
+    }
+
+    public fun sendToNewMember(sendToNewMember: Boolean): AnnouncementParametersBuilder {
+        this.sendToNewMember = sendToNewMember
+        return this
+    }
+
+    public fun pinned(isPinned: Boolean): AnnouncementParametersBuilder {
+        this.isPinned = isPinned
+        return this
+    }
+
+    public fun showEditCard(isShowEditCard: Boolean): AnnouncementParametersBuilder {
+        this.isShowEditCard = isShowEditCard
+        return this
+    }
+
+    public fun tip(isTip: Boolean): AnnouncementParametersBuilder {
+        this.isTip = isTip
+        return this
+    }
+
+    public fun needConfirm(needConfirm: Boolean): AnnouncementParametersBuilder {
+        this.needConfirm = needConfirm
+        return this
+    }
+
+    /**
+     * 使用当前参数构造 [AnnouncementParameters].
+     */
+    public fun build(): AnnouncementParameters =
+        AnnouncementParameters(image, sendToNewMember, isPinned, isShowEditCard, isTip, needConfirm)
+}
+
+/**
+ * 使用 [AnnouncementParametersBuilder] 构建 [AnnouncementParameters].
+ * @see AnnouncementParametersBuilder
+ *
+ * @since 2.7
+ */
+public inline fun buildAnnouncementParameters(
+    builderAction: AnnouncementParametersBuilder.() -> Unit
+): AnnouncementParameters = AnnouncementParametersBuilder().apply(builderAction).build()
+
+internal fun GroupAnnouncement.covertToAnnouncement(botId: Long): ReceiveAnnouncement {
+    check(this.fid != null) { "GroupAnnouncement don't have id" }
+    check(this.settings != null) { "GroupAnnouncement don't have setting" }
+
+    return ReceiveAnnouncement(
+        botId = botId,
+        fid = fid,
+        senderId = sender,
+        publishTime = time,
+        title = msg.title ?: "",
+        msg = msg.text,
+        readMemberNumber = readNum,
+        isAllRead = isAllConfirm != 0,
+        parameters = buildAnnouncementParameters {
+            isPinned = pinned == 1
+            sendToNewMember = type == 20
+            isTip = settings.tipWindowType == 0
+            needConfirm = settings.confirmRequired == 1
+            isShowEditCard = settings.isShowEditCard == 1
+        }
+    )
+}
+
+internal fun Announcement.covertToGroupAnnouncement(): GroupAnnouncement {
+    return GroupAnnouncement(
+        sender = botId,
+        msg = GroupAnnouncementMsg(
+            title = title,
+            text = msg
+        ),
+        type = if (parameters.sendToNewMember) 20 else 6,
+        settings = GroupAnnouncementSettings(
+            isShowEditCard = if (parameters.isShowEditCard) 1 else 0,
+            tipWindowType = if (parameters.isTip) 0 else 1,
+            confirmRequired = if (parameters.needConfirm) 1 else 0,
+        ),
+        pinned = if (parameters.isPinned) 1 else 0,
+    )
+}
\ No newline at end of file
diff --git a/mirai-core-api/src/commonMain/kotlin/data/GroupAnnouncement.kt b/mirai-core-api/src/commonMain/kotlin/data/GroupAnnouncement.kt
index ccdc72f4e..c85cfa61b 100644
--- a/mirai-core-api/src/commonMain/kotlin/data/GroupAnnouncement.kt
+++ b/mirai-core-api/src/commonMain/kotlin/data/GroupAnnouncement.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2020 Mamoe Technologies and contributors.
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
  *
  *  此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  *  Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -26,20 +26,22 @@ public data class GroupAnnouncementList(
     val ec: Int,  //状态码 0 是正常的
     @SerialName("em") val msg: String,   //信息
     val feeds: List<GroupAnnouncement>? = null,   //群公告列表
-    val inst: List<GroupAnnouncement>? = null  //置顶列表?
+    val inst: List<GroupAnnouncement>? = null  //置顶列表? 应该是发送给新成员的
 )
 
 @MiraiExperimentalApi
 @Serializable
 public data class GroupAnnouncement(
-    @SerialName("u") val sender: Long = 0,
+    @SerialName("u") val sender: Long = 0, //发送者id
     val msg: GroupAnnouncementMsg,
+    val type: Int = 0, //20 为inst , 6 为feeds
     val settings: GroupAnnouncementSettings? = null,
-    @SerialName("pubt") val time: Long = 0,
-    @SerialName("read_num") val readNum: Int = 0,
-    @SerialName("is_read") val isRead: Int = 0,
-    val pinned: Int = 0,
-    val fid: String? = null      //公告的id
+    @SerialName("pubt") val time: Long = 0, //发布时间
+    @SerialName("read_num") val readNum: Int = 0, //如果需要确认,则为确认收到的人数,反之则为已经阅读的人数
+    @SerialName("is_read") val isRead: Int = 0, //好像没用
+    @SerialName("is_all_confirm") val isAllConfirm: Int = 0, //为0 则未全部收到
+    val pinned: Int = 0, //1为置顶, 0为默认
+    val fid: String? = null,      //公告的id
 )
 
 @MiraiExperimentalApi
@@ -53,8 +55,16 @@ public data class GroupAnnouncementMsg(
 @MiraiExperimentalApi
 @Serializable
 public data class GroupAnnouncementSettings(
-    @SerialName("is_show_edit_card") val isShowEditCard: Int = 0,
+    @SerialName("is_show_edit_card") val isShowEditCard: Int = 0, //引导群成员修改该昵称  1 引导
     @SerialName("remind_ts") val remindTs: Int = 0,
-    @SerialName("tip_window_type") val tipWindowType: Int = 0,
-    @SerialName("confirm_required") val confirmRequired: Int = 0
+    @SerialName("tip_window_type") val tipWindowType: Int = 0,  //是否用弹窗展示   1 不使用
+    @SerialName("confirm_required") val confirmRequired: Int = 0 // 是否需要确认收到 1 需要
+)
+
+@MiraiExperimentalApi
+@Serializable
+public data class GroupAnnouncementImage(
+    @SerialName("h") val height: String,
+    @SerialName("w") val width: String,
+    @SerialName("id") val id: String
 )
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt
index a0f259eb5..2fbebc958 100644
--- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt
@@ -14,8 +14,11 @@ import io.ktor.client.engine.okhttp.*
 import io.ktor.client.features.*
 import io.ktor.client.request.*
 import io.ktor.client.request.forms.*
+import io.ktor.http.*
+import io.ktor.utils.io.core.*
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.withContext
 import kotlinx.io.core.discardExact
 import kotlinx.io.core.readBytes
 import kotlinx.serialization.json.*
@@ -56,6 +59,7 @@ import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_1
 import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_2
 import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import kotlin.io.use
 import kotlin.math.absoluteValue
 import kotlin.random.Random
 
@@ -592,6 +596,47 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
         isLenient = true
     }
 
+    @LowLevelApi
+    @MiraiExperimentalApi
+    override suspend fun uploadGroupAnnouncementImage(
+        bot: Bot,
+        groupId: Long,
+        resource: ExternalResource
+    ): GroupAnnouncementImage = bot.asQQAndroidBot().run {
+        //https://youtrack.jetbrains.com/issue/KTOR-455
+        val rep = Mirai.Http.post<String> {
+            url("https://web.qun.qq.com/cgi-bin/announce/upload_img")
+            body = MultiPartFormDataContent(formData {
+                append("\"bkn\"", bkn)
+                append("\"source\"", "troopNotice")
+                append("m", "0")
+                append(
+                    "\"pic_up\"",
+                    headers = Headers.build {
+                        append(HttpHeaders.ContentType, ContentType.Image.PNG)
+                        append(HttpHeaders.ContentDisposition, "filename=\"temp_uploadFile.png\"")
+                    }
+                ) {
+                    writeFully(resource.inputStream().withUse { readBytes() })
+                }
+            })
+            headers {
+                append(
+                    "cookie",
+                    " p_uin=o${id};" +
+                            " p_skey=${client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString() ?: error("cookie parse p_skey error")}; "
+                )
+            }
+        }
+        val jsonObj = json.parseToJsonElement(rep)
+        if (jsonObj.jsonObject["ec"]?.jsonPrimitive?.int != 0) {
+            throw IllegalStateException("Upload group announcement image fail group:$groupId msg:${jsonObj.jsonObject["em"]}")
+        }
+        val id = jsonObj.jsonObject["id"]?.jsonPrimitive?.content
+            ?: throw IllegalStateException("Upload group announcement image fail group:$groupId msg:${jsonObj.jsonObject["em"]}")
+        return json.decodeFromString(GroupAnnouncementImage.serializer(), id)
+    }
+
     @LowLevelApi
     @MiraiExperimentalApi
     override suspend fun sendGroupAnnouncement(bot: Bot, groupId: Long, announcement: GroupAnnouncement): String =
@@ -627,6 +672,53 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
                 ?: throw throw IllegalStateException("Send Announcement fail group:$groupId msg:${jsonObj.jsonObject["em"]} content:${announcement.msg.text}")
         }
 
+    @LowLevelApi
+    @MiraiExperimentalApi
+    override suspend fun sendGroupAnnouncementWithImage(
+        bot: Bot,
+        groupId: Long,
+        image: GroupAnnouncementImage,
+        announcement: GroupAnnouncement
+    ): String = bot.asQQAndroidBot().run {
+        val rep = withContext(network.coroutineContext) {
+            Mirai.Http.post<String> {
+                url("https://web.qun.qq.com/cgi-bin/announce/add_qun_notice")
+                body = MultiPartFormDataContent(formData {
+                    append("qid", groupId)
+                    append("bkn", bkn)
+                    append("text", announcement.msg.text)
+                    append("pinned", announcement.pinned)
+                    append("pic", image.id)
+                    append("imgWidth", image.width)
+                    append("imgHeight", image.height)
+                    append(
+                        "settings",
+                        json.encodeToString(
+                            GroupAnnouncementSettings.serializer(),
+                            announcement.settings ?: GroupAnnouncementSettings()
+                        )
+                    )
+                    append("format", "json")
+                })
+                headers {
+                    append(
+                        "cookie",
+                        " p_uin=o${id};" +
+                                " p_skey=${
+                                    client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString() ?: error(
+                                        "parse error"
+                                    )
+                                }; "
+                    )
+                }
+
+            }
+        }
+        val jsonObj = json.parseToJsonElement(rep)
+        return jsonObj.jsonObject["new_fid"]?.jsonPrimitive?.content
+            ?: throw throw IllegalStateException("Send Announcement with image fail group:$groupId msg:${jsonObj.jsonObject["em"]} content:${announcement.msg.text}")
+    }
+
     @LowLevelApi
     @MiraiExperimentalApi
     override suspend fun deleteGroupAnnouncement(bot: Bot, groupId: Long, fid: String) = bot.asQQAndroidBot().run {
diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt
index 6d2b56bc9..d71e311bc 100644
--- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt
@@ -12,11 +12,11 @@
 
 package net.mamoe.mirai.internal.contact
 
+import kotlinx.coroutines.flow.*
 import net.mamoe.mirai.LowLevelApi
 import net.mamoe.mirai.Mirai
 import net.mamoe.mirai.contact.*
-import net.mamoe.mirai.data.GroupInfo
-import net.mamoe.mirai.data.MemberInfo
+import net.mamoe.mirai.data.*
 import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.internal.QQAndroidBot
@@ -248,6 +248,29 @@ internal class GroupImpl(
         return result.success
     }
 
+    override suspend fun getAnnouncements(): Flow<ReceiveAnnouncement> =
+        flow {
+            var i = 1
+            while (true) {
+                val result = Mirai.getRawGroupAnnouncements(bot, id, i++)
+                check(result.ec == 0) { "Get Group Announcement error at page $i" }
+
+                if (result.inst.isNullOrEmpty() && result.feeds.isNullOrEmpty())
+                    return@flow
+
+                result.inst?.let { emitAll(it.asFlow()) }
+                result.feeds?.let { emitAll(it.asFlow()) }
+            }
+        }.map { it.covertToAnnouncement(bot.id) }
+
+    override suspend fun deleteAnnouncement(fid: String) {
+        checkBotPermission(MemberPermission.ADMINISTRATOR) { "Only administrator have permission to delete group announcement" }
+        Mirai.deleteGroupAnnouncement(bot, id, fid)
+    }
+
+    override suspend fun getAnnouncement(fid: String): ReceiveAnnouncement =
+        Mirai.getGroupAnnouncement(bot, id, fid).covertToAnnouncement(bot.id)
+
     override fun toString(): String = "Group($id)"
 }
 
@@ -278,3 +301,4 @@ internal fun GroupImpl.newAnonymous(name: String, id: String): AnonymousMemberIm
         anonymousId = id,
     )
 ) as AnonymousMemberImpl
+