Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
Microsoft Graph позволяет приложениям подписываться на ресурсы и получать уведомления об изменениях в ресурсах. В этой статье объясняется, как настроить расширенные уведомления, которые включают данные ресурсов непосредственно в полезные данные уведомления.
Расширенные уведомления устраняют необходимость в дополнительных вызовах API для получения обновленных ресурсов, что ускоряет и упрощает выполнение бизнес-логики.
Поддерживаемые ресурсы
Расширенные уведомления доступны для следующих ресурсов.
Примечание.
Расширенные уведомления для подписок на конечные точки, помеченные звездочкой (*), доступны только в конечной точке /beta .
| Ресурс | Поддерживаемые пути к ресурсам | Ограничения |
|---|---|---|
| Copilot aiInteraction | Взаимодействие с ИИ Copilot, частью чего является конкретный пользователь: copilot/users/{userId}/interactionHistory/getAllEnterpriseInteractions Взаимодействие Copilot AI в организации: copilot/interactionHistory/getAllEnterpriseInteractions |
Квоты максимальной подписки: |
| Событие Outlook | Изменения во всех событиях в почтовом ящике пользователя: /users/{id}/events |
Требует $select возвращать только подмножество свойств в расширенном уведомлении. Дополнительные сведения см. в разделе Уведомления об изменениях для ресурсов Outlook. |
| Сообщение Outlook | Изменения во всех сообщениях в почтовом ящике пользователя: /users/{id}/messages Изменения в сообщениях в папке "Входящие" пользователя: /users/{id}/mailFolders/{id}/messages |
Требует $select возвращать только подмножество свойств в расширенном уведомлении. Дополнительные сведения см. в разделе Уведомления об изменениях для ресурсов Outlook. |
| Личный контакт Outlook | Изменения во всех личных контактах в почтовом ящике пользователя: /users/{id}/contacts Изменения во всех личных контактах в папке contactFolder пользователя: /users/{id}/contactFolders/{id}/contacts |
Требует $select возвращать только подмножество свойств в расширенном уведомлении. Дополнительные сведения см. в разделе Уведомления об изменениях для ресурсов Outlook. |
| Вызовы TeamsRecording | Все записи в организации: communications/onlineMeetings/getAllRecordings Все записи для определенного собрания: communications/onlineMeetings/{onlineMeetingId}/recordings Запись звонка, которая становится доступной на собрании, организованном определенным пользователем: users/{id}/onlineMeetings/getAllRecordings Запись звонка, которая становится доступной на собрании, где установлено определенное приложение Teams: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllRecordings * |
Квоты максимальной подписки: |
| Вызов TeamsTranscript | Все расшифровки в организации: communications/onlineMeetings/getAllTranscripts Все расшифровки для определенного собрания: communications/onlineMeetings/{onlineMeetingId}/transcripts Расшифровка звонка, которая становится доступной на собрании, организованном определенным пользователем: users/{id}/onlineMeetings/getAllTranscripts Расшифровка звонка, которая становится доступной на собрании, где установлено определенное приложение Teams: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllTrancripts * |
Квоты максимальной подписки: |
| Канал Teams | Изменения каналов во всех командах: /teams/getAllChannelsИзменения канала в определенной команде: /teams/{id}/channels |
- |
| Чат Teams | Изменения в любом чате в клиенте: /chatsИзменения в конкретном чате: /chats/{id} |
- |
| chatMessage Teams | Изменения в сообщениях чата во всех каналах во всех командах: /teams/getAllMessagesИзменения в сообщениях чата в определенном канале: /teams/{id}/channels/{id}/messagesИзменения в сообщениях чата во всех чатах: /chats/getAllMessagesИзменения в сообщениях чата в определенном чате: /chats/{id}/messagesИзменения в сообщениях чата во всех чатах, в которые входит конкретный пользователь: /users/{id}/chats/getAllMessages |
Не поддерживает использование $select для возврата только выбранных свойств. Расширенное уведомление состоит из всех свойств измененного экземпляра. |
| conversationMember в Teams | Изменения членства в определенной команде: /teams/{id}/membersИзменения в членстве во всех командах в клиенте: /teams/getAllMembersИзменения членства во всех каналах в определенной команде: /teams/{id}/channels/getAllMembersИзменения членства для всех каналов во всем клиенте: /teams/getAllChannels/getAllMembersИзменения в членстве в определенном чате: /chats/{id}/members Изменения членства во всех чатах Teams: /chats/getAllMembers |
Не поддерживает использование $select для возврата только выбранных свойств. Расширенное уведомление состоит из всех свойств измененного экземпляра. |
| Teams onlineMeeting * | Изменения в онлайн-собрании: /communications/onlineMeetings(joinWebUrl='{encodedJoinWebUrl}')/meetingCallEvents * |
Не поддерживает использование $select для возврата только выбранных свойств. Расширенное уведомление состоит из всех свойств измененного экземпляра. Одна подписка разрешена для каждого приложения на собрание по сети. Дополнительные сведения см. в разделе Получение уведомлений об изменениях для обновлений событий звонков в Microsoft Teams. |
| presence в Teams | Изменения в присутствии одного пользователя: /communications/presences/{id} Изменения в присутствии нескольких пользователей: /communications/presences?$filter=id in ({id},{id}...) |
Подписка на присутствие нескольких пользователей ограничена 650 отдельными пользователями. Не поддерживает использование $select для возврата только выбранных свойств. Расширенное уведомление состоит из всех свойств измененного экземпляра. Допускается одна подписка на одно приложение на каждого делегированного пользователя. Дополнительные сведения см. в разделе Получение уведомлений об изменениях для обновлений присутствия в Microsoft Teams. |
| Команда Teams | Изменения в любой команде в клиенте: /teamsИзменения в конкретной команде: /teams/{id} |
- |
Сведения ресурсов в полезных данных уведомлений
Расширенные уведомления включают данные ресурсов со следующими сведениями:
- Идентификатор и тип измененного экземпляра ресурса, найденные в свойстве resourceData .
- Все значения свойств экземпляра ресурса, зашифрованные, как указано в подписке, находятся в свойстве encryptedContent .
- Конкретные свойства ресурса в зависимости от ресурса или запроса с помощью
$selectпараметра в URL-адресе ресурса подписки.
Настройка приложения для уведомлений
Перед созданием подписки с данными ресурса настройте доступ к приложению для объекта субъекта-службы, представляющего пару "клиент— приложение", задав свойство appRoleAssignmentRequired следующим образом:
- [Рекомендуется] Задав для него значение
false. Узнайте, как настроить это свойство. - Кроме того, если свойство должно оставаться
true, явно назначьте субъекту-службе Microsoft Graph Отслеживание изменений (appId —0bf30f3b-4a52-48df-9a82-234910c4a086) роль приложения ресурса, поддерживаемую ресурсом Microsoft Graph. Узнайте, как предоставить роли приложения субъекту-службе.
Если ни один из условий не выполняется, полезные данные уведомления будут содержать nullмаркер проверки.
Создание подписки
Чтобы настроить расширенные уведомления, выполните те же действия, что и базовые уведомления об изменениях, но укажите следующие обязательные свойства:
-
includeResourceData: задайте значение для
trueзапроса данных ресурсов. - encryptionCertificate. Укажите открытый ключ, который Microsoft Graph использует для шифрования данных ресурса. Дополнительные сведения см. в статье Расшифровка данных ресурсов из уведомлений об изменениях.
- encryptionCertificateId. Укажите идентификатор сертификата для сопоставления уведомлений с правильным ключом расшифровки.
Проверьте обе конечные точки, как описано в ст. Проверка конечной точки уведомлений. Если вы используете один и тот же URL-адрес для обеих конечных точек, вы получите и должны ответить на два запроса на проверку.
Пример. Запрос подписки
В этом примере создается подписка на сообщения канала в Microsoft Teams.
POST https://graph.microsoft.com/v1.0/subscriptions
Content-Type: application/json
{
"changeType": "created,updated",
"notificationUrl": "https://webhook.azurewebsites.net/api/resourceNotifications",
"resource": "/teams/{id}/channels/{id}/messages",
"includeResourceData": true,
"encryptionCertificate": "{base64encodedCertificate}",
"encryptionCertificateId": "{customId}",
"expirationDateTime": "2019-09-19T11:00:00.0000000Z",
"clientState": "{secretClientState}"
}
Отклик подписки
HTTP/1.1 201 Created
Content-Type: application/json
{
"changeType": "created,updated",
"notificationUrl": "https://webhook.azurewebsites.net/api/resourceNotifications",
"resource": "/teams/{id}/channels/{id}/messages",
"includeResourceData": true,
"encryptionCertificateId": "{customId}",
"expirationDateTime": "2019-09-19T11:00:00.0000000Z",
"clientState": "{secretClientState}"
}
Уведомления жизненного цикла подписки
События могут нарушить поток уведомлений об изменениях в подписке. Уведомления жизненного цикла сообщают о действиях, которые следует предпринять, чтобы поток не прерывался. В отличие от уведомлений об изменении ресурсов, уведомления о жизненном цикле сосредоточены на состоянии подписки.
Дополнительные сведения см. в статье Уменьшение количества недостающих подписок и уведомлений об изменениях.
Проверка подлинности уведомлений
Всегда проверяйте подлинность уведомлений об изменениях перед их обработкой. Это не позволит приложению активировать неправильную бизнес-логику с помощью поддельных уведомлений от третьих лиц.
Для базовых уведомлений проверьте их, используя значение clientState , как описано в разделе Обработка уведомления об изменениях. Для расширенных уведомлений выполните дополнительные действия по проверке.
Маркеры проверки в уведомлении об изменениях
Расширенные уведомления включают свойство validationTokens , содержащее массив веб-маркеров JSON (JWT). Каждый токен уникален для пары приложения и клиента. Уведомление об изменениях может содержать набор элементов для различных приложений и клиентов, которые подписаны с помощью одного и того же notificationUrl.
Примечание.
Microsoft Graph не отправляет маркеры проверки для уведомлений об изменениях, доставленных через Центры событий Azure, так как службе подписки не нужно проверять notificationUrl для Центров событий.
В следующем примере уведомление об изменении содержит два элемента для одного приложения и двух разных клиентов, поэтому массив validationTokens содержит два маркера, требующих проверки.
Совет
Значение nullvalidationTokens указывает, что Microsoft Graph не удалось зашифровать данные ресурса из-за неправильной конфигурации приложения. Чтобы устранить эту проблему, ознакомьтесь с разделом Конфигурация приложения для уведомлений .
{
"value": [
{
"subscriptionId": "76619225-ff6b-4489-96ca-4ef547e78b22",
"tenantId": "aaaabbbb-0000-cccc-1111-dddd2222eeee",
"changeType": "created",
...
},
{
"subscriptionId": "5cfe2387-163c-4006-81bb-1b5e1e060afe",
"tenantId": "bbbbcccc-1111-dddd-2222-eeee3333ffff",
"changeType": "created",
...
}
],
"validationTokens": [
"eyJ0eXAiOiJKV1QiLCJhb...",
"cGlkYWNyIjoiMiIsImlkc..."
]
}
Объект уведомления об изменениях находится в структуре типа ресурса changeNotificationCollection.
Способ проверки
Используйте библиотеку проверки подлинности Майкрософт (MSAL) или стороннюю библиотеку для проверки маркеров. Выполните следующие действия:
Помните о следующих принципах:
- Немедленно ответьте на уведомление кодом
HTTP 202 Acceptedсостояния. - Ответьте перед проверкой уведомления об изменениях, даже если проверка завершается неудачей позже. Отвечайте сразу после получения уведомления об изменениях, независимо от того, храните ли вы уведомления в очередях для последующей обработки или обработки на лету.
- Принятие уведомления об изменениях и реагирование на них предотвращает ненужные повторные попытки доставки и скрывает результаты проверки от потенциальных злоумышленников. Вы всегда можете игнорировать уведомление о недопустимом изменении после его получения.
В частности, выполняйте проверку каждого маркера JWT в коллекции validationTokens. Если любой из маркеров не прошел проверку, считайте уведомление об изменении подозрительным и выполните дальнейшее исследование.
Выполните следующие действия, чтобы проверить маркеры и приложения, которые их создают:
Убедитесь, что срок действия маркера не истек.
Убедитесь, что платформа удостоверений Майкрософт выдал маркер и что он не был изменен.
- Получите ключи подписи от общей конечной точки конфигурации:
https://login.microsoftonline.com/common/.well-known/openid-configuration. Приложение может кэшировать эту конфигурацию в течение некоторого времени. Конфигурация часто обновляется, так как ключи подписывания сменяются ежедневно. - Проверьте подпись маркера JWT, использующего эти ключи.
Не принимайте маркеры, выданные каким-либо другим центром.
- Получите ключи подписи от общей конечной точки конфигурации:
Убедитесь, что маркер был выдан для вашего приложения.
Следующие действия являются частью стандартной логики проверки в библиотеках маркеров JWT, и обычно их можно выполнять в виде вызова одной функции.
- Убедитесь, что "аудитория" в маркере совпадает с идентификатором вашего приложения.
- Если уведомления об изменениях получают несколько приложений, выполните проверку по нескольким идентификаторам.
Убедитесь, что свойство маркера
azpсоответствует ожидаемому значению0bf30f3b-4a52-48df-9a82-234910c4a086, которое представляет издателя уведомлений об изменениях Microsoft Graph.
Пример маркера JWT
В следующем примере показаны свойства маркера JWT, необходимые для проверки.
{
// aud is your app's id
"aud": "925bff9f-f6e2-4a69-b858-f71ea2b9b6d0",
"iss": "https://login.microsoftonline.com/9f4ebab6-520d-49c0-85cc-7b25c78d4a93/v2.0",
"iat": 1624649764,
"nbf": 1624649764,
"exp": 1624736464,
"aio": "E2ZgYGjnuFglnX7mtjJzwR5lYaWvAA==",
// azp represents the notification publisher and must always be the same value of 0bf30f3b-4a52-48df-9a82-234910c4a086
"azp": "0bf30f3b-4a52-48df-9a82-234910c4a086",
"azpacr": "2",
"oid": "1e7d79fa-7893-4d50-bdde-164260d9c5ba",
"rh": "0.AX0AtrpOnw1SwEmFzHslx41KkzsP8wtSSt9ImoIjSRDEoIZ9AAA.",
"sub": "1e7d79fa-7893-4d50-bdde-164260d9c5ba",
"tid": "9f4ebab6-520d-49c0-85cc-7b25c78d4a93",
"uti": "mIB4QKCeZE6hK71XUHJ3AA",
"ver": "2.0"
}
Пример: подтверждение маркеров проверки
// add Microsoft.IdentityModel.Protocols.OpenIdConnect and System.IdentityModel.Tokens.Jwt nuget packages to your project
public async Task<bool> ValidateToken(string token, string tenantId, IEnumerable<string> appIds)
{
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever());
var openIdConfig = await configurationManager.GetConfigurationAsync();
var handler = new JwtSecurityTokenHandler();
try
{
handler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidIssuer = $"https://sts.windows.net/{tenantId}/",
ValidAudiences = appIds,
IssuerSigningKeys = openIdConfig.SigningKeys
}, out _);
return true;
}
catch (Exception ex)
{
Trace.TraceError($"{ex.Message}:{ex.StackTrace}");
return false;
}
}
Расшифровка данных ресурсов из уведомлений об изменениях
Свойство resourceData в уведомлении об изменениях содержит базовые сведения об идентификаторе и типе экземпляра ресурса. Свойство encryptedData содержит полные данные ресурса, зашифрованные Microsoft Graph с открытым ключом, указанным в подписке. Это свойство также содержит значения, необходимые для проверки и расшифровки. Это шифрование выполняется для повышения безопасности данных клиентов, к которые обращаются через уведомления об изменениях. Защитите закрытый ключ, чтобы третья сторона не могла расшифровать данные клиента, даже если она перехватывает исходные уведомления об изменениях.
В этом разделе вы узнаете о следующих понятиях:
Управление ключами шифрования
Получите сертификат с парой асимметричных ключей.
Вы можете использовать самозаверяющий сертификат, так как Microsoft Graph не проверяет издателя сертификата и использует открытый ключ только для шифрования.
Используйте Azure Key Vault для создания, смены сертификатов и безопасного управления ими. Убедитесь, что ключи удовлетворяют следующим условиям:
- Ключ должен иметь тип
RSA. - Размер ключа должен находиться в диапазоне от 2048 до 4096 бит.
- Ключ должен иметь тип
Экспортируйте сертификат в формате X.509 в кодировке Base64 и включите только открытый ключ.
При создании подписки:
Укажите сертификат в свойстве encryptionCertificate , используя содержимое в кодировке Base64, в которое был экспортирован сертификат.
Укажите ваш собственный идентификатор в свойстве encryptionCertificateId.
Этот идентификатор позволяет сопоставлять сертификаты с получаемыми уведомлениями об изменениях, а также получать сертификаты из хранилища сертификатов. Длина идентификатора не должна превышать 128 символов.
Обеспечьте защиту закрытого ключа, чтобы ваш код обработки уведомлений об изменениях мог обращаться к закрытому ключу для расшифровки данных ресурсов.
Ротация ключей
Периодически изменяйте асимметричные ключи, чтобы свести к минимуму риск компрометации закрытого ключа. Чтобы добавить новую пару ключей, выполните следующие действия:
Получите новый сертификат с новой парой асимметричных ключей. Используйте его для всех создаваемых подписок.
Обновите существующие подписки с использованием нового ключа сертификата.
- Сделайте это обновление частью регулярного продления подписки.
- Или перечислите все подписки и укажите ключ. Используйте операцию PATCH для подписки и обновите свойства encryptionCertificate и encryptionCertificateId.
Помните о следующих принципах.
- Старый сертификат может по-прежнему использоваться для шифрования в течение некоторого времени. Чтобы расшифровать контент, у вашего приложения должен быть доступ как к старым, так и к новым сертификатам.
- Используйте свойство encryptionCertificateId в каждом уведомлении об изменении, чтобы определить правильный ключ для использования.
- Отмена старого сертификата только в том случае, если вы не видите последних уведомлений об изменениях, ссылающихся на него.
Расшифровка данных ресурсов
Microsoft Graph использует двухэтапный процесс шифрования с целью оптимизации работы:
- Он создает одноразовый симметричный ключ и использует его для шифрования данных ресурсов.
- Используется открытый асимметричный ключ (указанный при подписке), чтобы зашифровать симметричный ключ и добавить его в каждое уведомление об изменении этой подписки.
Предположим, симметричный ключ отличается для каждого элемента в уведомлении об изменении.
Чтобы расшифровать данные ресурсов, приложение должно выполнить обратные действия, используя свойства в разделе encryptedContent в каждом уведомлении об изменении:
Определите правильный сертификат с помощью свойства encryptionCertificateId .
Инициализация криптографического компонента RSA с помощью закрытого ключа. Простой способ инициализации компонента RSA — использовать метод RSACertificateExtensions.GetRSAPrivateKey(X509Certificate2) с экземпляром X509Certificate2 , который содержит закрытый ключ, описанный в разделе Управление ключами шифрования.
Расшифруйте симметричный ключ в свойстве dataKey каждого элемента в уведомлении об изменениях с помощью закрытого ключа. Используйте оптимальное заполнение асимметричного шифрования (OAEP) в качестве алгоритма расшифровки.
Используйте симметричный ключ для вычисления сигнатуры HMAC-SHA256 для значения в данных. Сравните его со значением в объекте dataSignature. Если они не совпадают, предположим, что полезные данные были изменены и не расшифровывайте их.
Расшифруйте свойство данных с помощью симметричного ключа с помощью расширенного Standard шифрования (AES), например Aes .NET.
Используйте следующие параметры расшифровки для алгоритма AES:
- Заполнение: PKCS7.
- Режим шифра: CBC.
Настройте "вектор инициализации", скопировав первые 16 байт симметричного ключа, использованного для расшифровки.
Расшифрованные данные будут представлять собой строку JSON, представляющую ресурс.
Пример. Расшифровка данных ресурсов
В следующем примере JSON показано уведомление об изменении, содержащее зашифрованные значения свойств экземпляра chatMessage в сообщении канала. Значение @odata.id указывает экземпляр .
{
"value": [
{
"subscriptionId": "76222963-cc7b-42d2-882d-8aaa69cb2ba3",
"changeType": "created",
// Other properties typical in a resource change notification
"resource": "teams('d29828b8-c04d-4e2a-b2f6-07da6982f0f0')/channels('19:f127a8c55ad949d1a238464d22f0f99e@thread.skype')/messages('1565045424600')/replies('1565047490246')",
"resourceData": {
"id": "1565293727947",
"@odata.type": "#Microsoft.Graph.ChatMessage",
"@odata.id": "teams('88cbc8fc-164b-44f0-b6a6-b59b4a1559d3')/channels('19:8d9da062ec7647d4bb1976126e788b47@thread.tacv2')/messages('1565293727947')/replies('1565293727947')"
},
"encryptedContent": {
"data": "{encrypted data that produces a full resource}",
"dataSignature": "<HMAC-SHA256 hash>",
"dataKey": "{encrypted symmetric key from Microsoft Graph}",
"encryptionCertificateId": "MySelfSignedCert/DDC9651A-D7BC-4D74-86BC-A8923584B0AB",
"encryptionCertificateThumbprint": "07293748CC064953A3052FB978C735FB89E61C3D"
}
}
],
"validationTokens": [
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSU..."
]
}
Полное описание данных, отправляемых при доставке уведомлений об изменениях, см. в разделе Тип ресурса changeNotificationCollection.
Расшифровка симметричного ключа
В этом разделе содержатся некоторые полезные фрагменты кода, использующие C# и .NET для каждого этапа расшифровки.
// Initialize with the private key that matches the encryptionCertificateId.
X509Certificate2 certificate = <instance of X509Certificate2 matching the encryptionCertificateId property>;
RSA rsa = certificate.GetRSAPrivateKey();
byte[] encryptedSymmetricKey = Convert.FromBase64String(<value from dataKey property>);
// Decrypt using OAEP padding.
byte[] decryptedSymmetricKey = rsa.Decrypt(encryptedSymmetricKey, RSAEncryptionPadding.OaepSHA1);
// Can now use decryptedSymmetricKey with the AES algorithm.
Сравнение подписи данных с помощью HMAC-SHA256
byte[] decryptedSymmetricKey = <the aes key decrypted in the previous step>;
byte[] encryptedPayload = <the value from the data property, still encrypted>;
byte[] expectedSignature = <the value from the dataSignature property>;
byte[] actualSignature;
using (HMACSHA256 hmac = new HMACSHA256(decryptedSymmetricKey))
{
actualSignature = hmac.ComputeHash(encryptedPayload);
}
if (actualSignature.SequenceEqual(expectedSignature))
{
// Continue with decryption of the encryptedPayload.
}
else
{
// Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
}
Расшифровка содержимого данных ресурсов
Aes aesProvider = Aes.Create();
aesProvider.Key = decryptedSymmetricKey;
aesProvider.Padding = PaddingMode.PKCS7;
aesProvider.Mode = CipherMode.CBC;
// Obtain the initialization vector from the symmetric key itself.
int vectorSize = 16;
byte[] iv = new byte[vectorSize];
Array.Copy(decryptedSymmetricKey, iv, vectorSize);
aesProvider.IV = iv;
byte[] encryptedPayload = Convert.FromBase64String(<value from data property>);
string decryptedResourceData;
// Decrypt the resource data content.
using (var decryptor = aesProvider.CreateDecryptor())
{
using (MemoryStream msDecrypt = new MemoryStream(encryptedPayload))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
decryptedResourceData = srDecrypt.ReadToEnd();
}
}
}
}
// decryptedResourceData now contains a JSON string that represents the resource.
Связанные материалы
- Настройте тип ресурса подписки.