- Remove CAS, as it no longer works for us - Remove other checks - Implement 15 minute greet timeout check - Fix chat logos - Ditch thirdparty logger, use stdlib log facility wrapper for logging
131 lines
4.7 KiB
Go
131 lines
4.7 KiB
Go
package moon
|
|
|
|
import (
|
|
"bunny-bot/internal/mylog"
|
|
|
|
"github.com/NicoNex/echotron/v3"
|
|
"github.com/davecgh/go-spew/spew"
|
|
)
|
|
|
|
// The Bot API always returns a User/Chat carrying its id, and echotron validates
|
|
// nothing: lclient.dispatch (network.go:138) is json.Unmarshal plus a check() that
|
|
// reads only the ok/error_code envelope (helpers.go:37). An entity that arrives
|
|
// without an id therefore reaches us as a silent zero, with no layer below to
|
|
// complain about it. These predicates are the whole of our defence.
|
|
//
|
|
// They are SILENT. A refusal is a fact, not an incident: reporting happens once, at
|
|
// the ingress, where the offending update can be dumped whole. A predicate that logs
|
|
// would make one broken update produce one line per entity that happens to meet it.
|
|
|
|
// usableUser reports whether a user entity is one we can actually address.
|
|
//
|
|
// The test is against zero and not against zero-or-less on purpose: a chat id is
|
|
// legitimately negative for a group or supergroup, so a positivity check would
|
|
// refuse every group chat in the project.
|
|
func usableUser(u *echotron.User) bool {
|
|
return u != nil && u.ID != 0
|
|
}
|
|
|
|
// usableChat reports whether a chat entity is one we can actually address, with the
|
|
// same reasoning and the same zero-not-positive rule.
|
|
func usableChat(c *echotron.Chat) bool {
|
|
return c != nil && c.ID != 0
|
|
}
|
|
|
|
// usableUpdate reports whether an update is worth dispatching at all.
|
|
//
|
|
// Its branches mirror telegramMsgParser's, in the same order and under the same
|
|
// conditions, so a branch added to one and not the other is visible by reading. That
|
|
// is not tidiness: the alternative is a validator shaped differently from the
|
|
// dispatcher, whose coverage nobody can check by eye.
|
|
//
|
|
// nil is tolerated only where absence is normal. A message with no author is a
|
|
// channel post or a service message, and refusing those would disable chat logging
|
|
// for channels. A nil element inside NewChatMembers is different — the array asserts
|
|
// a newcomer exists — and so is a nil NewChatMember.User, which asserts who changed.
|
|
// A nil OldChatMember.User is tolerated: some transitions have no previous member.
|
|
func usableUpdate(u *echotron.Update) bool {
|
|
if u == nil {
|
|
return false
|
|
}
|
|
|
|
switch {
|
|
case u.ChatMember != nil:
|
|
return usableChatMember(u.ChatMember)
|
|
case u.Message != nil:
|
|
return usableMessage(u.Message)
|
|
case u.EditedMessage != nil:
|
|
return usableMessage(u.EditedMessage)
|
|
default:
|
|
// Nothing we act on. The dispatcher's spew fallback is where this belongs.
|
|
return true
|
|
}
|
|
}
|
|
|
|
// usableChatMember checks a membership change.
|
|
//
|
|
// Its entities are values, not pointers: ChatMemberUpdated.Chat, .From and both
|
|
// ChatMember structs can never be nil, so there is no nil test to fall back on and
|
|
// the id check is the only thing that can catch a malformed one.
|
|
func usableChatMember(cm *echotron.ChatMemberUpdated) bool {
|
|
return usableChat(&cm.Chat) &&
|
|
usableUser(cm.NewChatMember.User) &&
|
|
(cm.OldChatMember.User == nil || usableUser(cm.OldChatMember.User)) &&
|
|
usableUser(&cm.From)
|
|
}
|
|
|
|
// usableMessage checks a message, or an edit of one — the two carry the same fields
|
|
// and are treated the same way.
|
|
//
|
|
// Message.Chat is a value: a zero chat id would otherwise reach elog, which builds
|
|
// chat_logs/<id>/yyyy/mm/dd.txt from it and would silently create a chat_logs/0
|
|
// directory and write every malformed update into it.
|
|
func usableMessage(m *echotron.Message) bool {
|
|
if !usableChat(&m.Chat) {
|
|
return false
|
|
}
|
|
|
|
if m.From != nil && !usableUser(m.From) {
|
|
return false
|
|
}
|
|
|
|
if m.LeftChatMember != nil && !usableUser(m.LeftChatMember) {
|
|
return false
|
|
}
|
|
|
|
for _, member := range m.NewChatMembers {
|
|
if !usableUser(member) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// One level deep, and the nested chat only. The nested author is never rendered,
|
|
// so a zero there is cosmetic; the level bound exists to keep the work constant.
|
|
if m.PinnedMessage != nil && !usableChat(&m.PinnedMessage.Chat) {
|
|
return false
|
|
}
|
|
|
|
if m.ReplyToMessage != nil && !usableChat(&m.ReplyToMessage.Chat) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// reportBrokenUpdate records an update we are refusing, in the terms it deserves:
|
|
// this is not a user being difficult and not a transient API error, it is an
|
|
// upstream that broke its own contract.
|
|
//
|
|
// The whole update is dumped, once. The dump is the point — the condition is
|
|
// unreproducible and unqueryable, since there is no id to ask Telegram about — and a
|
|
// partial dump of one field out of four would not be evidence of anything.
|
|
func reportBrokenUpdate(u *echotron.Update) {
|
|
mylog.Error(
|
|
"Telegram sent an update with an entity carrying no id, refusing it: nothing in it can be greeted, recorded or banned, and an API that sends one is broken",
|
|
)
|
|
|
|
mylog.Error(spew.Sdump(u))
|
|
}
|
|
|
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|